File Coverage

blib/lib/Hypersonic/Event.pm
Criterion Covered Total %
statement 32 56 57.1
branch 14 38 36.8
condition 2 17 11.7
subroutine 7 11 63.6
pod 6 7 85.7
total 61 129 47.2


line stmt bran cond sub pod time code
1             package Hypersonic::Event;
2              
3 19     19   218116 use strict;
  19         32  
  19         684  
4 19     19   77 use warnings;
  19         27  
  19         1019  
5 19     19   316 use 5.010;
  19         59  
6              
7             our $VERSION = '0.19';
8              
9             # Hypersonic::Event - Event backend registry and selection
10             #
11             # This module provides a central registry for event loop backends and
12             # automatic detection of the best available backend for the platform.
13             # All event loop implementations are JIT-compiled via XS::JIT::Builder.
14              
15             # Backend registry - maps name to module
16             my %BACKENDS = (
17             io_uring => 'Hypersonic::Event::IOUring', # Linux 5.1+ (fastest)
18             epoll => 'Hypersonic::Event::Epoll', # Linux (fast)
19             kqueue => 'Hypersonic::Event::Kqueue', # BSD/macOS (fast)
20             iocp => 'Hypersonic::Event::IOCP', # Windows (fast, completion-based)
21             event_ports => 'Hypersonic::Event::EventPorts', # Solaris/illumos (fast)
22             poll => 'Hypersonic::Event::Poll', # POSIX fallback
23             select => 'Hypersonic::Event::Select', # Universal (Windows support)
24             );
25              
26             # Priority order for auto-selection (first available wins).
27             #
28             # io_uring is first on Linux because it batches submissions through
29             # the SQ ring (one syscall per arm-poll batch vs one syscall per
30             # epoll_ctl), but as of 0.19 it operates in *readiness-only* mode -
31             # we use io_uring_prep_poll_add (level-triggered POLLIN) as a pure
32             # notification mechanism and let the main event loop's userspace
33             # accept()/recv() do the actual I/O, exactly like the epoll path.
34             #
35             # Pre-0.19 attempted to use io_uring's completion-based I/O
36             # (io_uring_prep_accept + io_uring_prep_recv) where the kernel did
37             # the I/O and returned the result via cqe->res, but that had two
38             # unfixable bugs:
39             # (a) gen_get_fd discarded the accepted client_fd from cqe->res
40             # for UD_ACCEPT and set fd=listen_fd, so the main loop's
41             # accept(listen_fd) returned EAGAIN (kernel already had it)
42             # and broke - leaking the connection.
43             # (b) prep_recv used a single GLOBAL recv_buf shared across all
44             # concurrent clients, corrupting each other's request data.
45             # This produced the "empty body + 18 SIGKILL cascade + 5140s
46             # wallclock" pattern in CPAN tester reports for Hypersonic 0.18 on
47             # cpansmoker-1023 (perl 5.38..5.43). The readiness-only design in
48             # 0.19 sidesteps both bugs.
49             #
50             # If a user's kernel doesn't behave as expected, the env var
51             # HYPERSONIC_EVENT_BACKEND=epoll (or any registered backend) and
52             # the constructor option `event_backend => 'epoll'` both override
53             # auto-detection.
54             my @PRIORITY = qw(io_uring epoll kqueue iocp event_ports poll select);
55              
56             # Check if io_uring is available (Linux 5.1+ with liburing)
57             sub _has_io_uring {
58 0 0   0   0 return 0 unless $^O eq 'linux';
59              
60             # Check kernel version >= 5.1
61 0   0     0 my $ver = `uname -r 2>/dev/null` || '';
62 0         0 my ($major, $minor) = $ver =~ /^(\d+)\.(\d+)/;
63 0 0 0     0 return 0 unless $major && ($major > 5 || ($major == 5 && $minor >= 1));
      0        
64              
65             # Check for liburing.h in common locations
66 0         0 for my $path (
67             '/usr/include/liburing.h',
68             '/usr/local/include/liburing.h',
69             '/usr/include/x86_64-linux-gnu/liburing.h',
70             ) {
71 0 0       0 return 1 if -f $path;
72             }
73              
74 0         0 return 0;
75             }
76              
77             # Select best available backend for this platform
78             sub best_backend {
79 47     47 1 16922 my $class = shift;
80              
81             # Explicit override via env var. Useful for (a) developers who
82             # know io_uring works on their kernel and want the throughput,
83             # and (b) test runners that need to pin a known-good backend.
84             # Validated against the registered backend list - a bogus value
85             # falls back to auto-detection rather than dying, so a typo in
86             # a user's shell rc doesn't blow up production.
87 47 50       241 if (my $forced = $ENV{HYPERSONIC_EVENT_BACKEND}) {
88 0 0       0 if ($BACKENDS{$forced}) {
89 0         0 my $mod = $BACKENDS{$forced};
90 0         0 eval "require $mod";
91 0 0 0     0 return $forced if !$@ && $mod->available;
92             # Fall through to auto-detect if the requested backend
93             # isn't actually loadable / available on this host.
94             }
95             }
96              
97             # Windows prefers IOCP (falls back to select if unavailable)
98 47 50       222 if ($^O eq 'MSWin32') {
99 0         0 my $mod = $BACKENDS{iocp};
100 0         0 eval "require $mod";
101 0 0 0     0 return 'iocp' if !$@ && $mod->available;
102 0         0 return 'select';
103             }
104              
105 47         179 for my $name (@PRIORITY) {
106 94 50       599 my $mod = $BACKENDS{$name} or next;
107              
108             # Try to load the module
109 94         14339 eval "require $mod";
110 94 50       567 next if $@;
111              
112             # Check if it's available on this platform
113 94 100       823 next unless $mod->available;
114              
115 47         693 return $name;
116             }
117              
118 0         0 die "No event backend available for platform: $^O";
119             }
120              
121             # Get backend module by name (loads if needed)
122             sub backend {
123 49     49 1 15888 my ($class, $name) = @_;
124 49   66     198 $name //= $class->best_backend;
125              
126 49 100       246 my $mod = $BACKENDS{$name}
127             or die "Unknown event backend: $name (available: " . join(', ', sort keys %BACKENDS) . ")";
128              
129 48 50       3429 eval "require $mod" or die "Cannot load $mod: $@";
130              
131 48 50       315 die "$mod is not available on this platform"
132             unless $mod->available;
133              
134 48         165 return $mod;
135             }
136              
137             # List all backends that work on this system
138             sub available_backends {
139 2     2 1 3409 my $class = shift;
140 2         4 my @available;
141              
142 2         7 for my $name (@PRIORITY) {
143 14 50       71 my $mod = $BACKENDS{$name} or next;
144              
145 14         1124 eval "require $mod";
146 14 50       63 next if $@;
147              
148 14 100       112 push @available, $name if $mod->available;
149             }
150              
151 2         11 return @available;
152             }
153              
154             # Return the backend priority order
155             sub backend_priority {
156 1     1 0 174386 return @PRIORITY;
157             }
158              
159             # List all registered backends (whether available or not)
160             sub all_backends {
161 0     0 1   return sort keys %BACKENDS;
162             }
163              
164             # Register a custom backend
165             sub register_backend {
166 0     0 1   my ($class, $name, $module) = @_;
167              
168 0 0         die "Backend name required" unless $name;
169 0 0         die "Module name required" unless $module;
170              
171 0           $BACKENDS{$name} = $module;
172              
173 0           return 1;
174             }
175              
176             # Unregister a backend
177             sub unregister_backend {
178 0     0 1   my ($class, $name) = @_;
179 0           delete $BACKENDS{$name};
180             }
181              
182             1;
183              
184             __END__