polyphony 1.0.2 → 1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09f35eaafb1ff569e8a36e1b1e2350e52ce3048b814fb8ccca935ed1a45d1dbd'
4
- data.tar.gz: 0e2296097fdb1d6604c6a41a08880d0cab62052cab4b1b4ebe2ac2ce92bbb737
3
+ metadata.gz: 61fa840f595a0e75da1d6e1b761506b18fa90d5a94b25ce5832d22aae2e08fbd
4
+ data.tar.gz: 7d3a79225f68bb889ff0491ced1249c2679a222a3f79a5c4cf48f9571865ebc4
5
5
  SHA512:
6
- metadata.gz: 80004fb81f991c8cca2c65bf89ed642f90f0e4947a47ea7ac34b59200aebc28e5a73215e8c80e837cd22edf92cf8026332a7df4722a5f5555956648ad5e8c9ee
7
- data.tar.gz: 3eb8022b4fab6b344b422de7791260b1036bd147f7ad8f9a4942e398f0071d8c3468ece6f7d576bb167b4333477643fe1e1b3348601d6fb9db164d8d49775328
6
+ metadata.gz: 32d61cf7e0858e704fc39fe317048e3e531afcf97da70b9b3d1e4b59a078e2f5c8c65a3f72c656b61285df9e5c99d7430938403560ca6e8e7aa5d920dada274e
7
+ data.tar.gz: c5c1488b1cec55810ffccf8116d70e8312ccb7d93875c1047ab7608595eb81fda9601f44ef308b3e1f35319d683b6ceda423284c9cda76c6f44882a7341e681a
@@ -8,7 +8,7 @@ jobs:
8
8
  fail-fast: false
9
9
  matrix:
10
10
  os: [ubuntu-latest, macos-latest]
11
- ruby: ['3.0', '3.1', '3.2']
11
+ ruby: ['3.0', '3.1', '3.2', 'head']
12
12
 
13
13
  name: >-
14
14
  ${{matrix.os}}, ${{matrix.ruby}}
@@ -8,7 +8,7 @@ jobs:
8
8
  fail-fast: false
9
9
  matrix:
10
10
  os: [ubuntu-latest]
11
- ruby: ['3.0', '3.1', '3.2']
11
+ ruby: ['3.0', '3.1', '3.2', 'head']
12
12
 
13
13
  name: >-
14
14
  ${{matrix.os}}, ${{matrix.ruby}}
data/.yardopts CHANGED
@@ -20,6 +20,7 @@
20
20
  docs/readme.md
21
21
  docs/overview.md
22
22
  docs/tutorial.md
23
+ docs/advanced-io.md
23
24
  docs/cheat-sheet.md
24
25
  docs/faq.md
25
26
  docs/concurrency.md
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 1.1 2023-06-08
2
+
3
+ - Add advanced I/O doc page
4
+ - Add `Fiber#receive_loop` API
5
+
1
6
  ## 1.0.2 2023-05-28
2
7
 
3
8
  - Remove liburing man files from gemspec (#103)
data/README.md CHANGED
@@ -77,6 +77,7 @@ $ gem install polyphony
77
77
 
78
78
  - [Overview](docs/overview.md)
79
79
  - [Tutorial](docs/tutorial.md)
80
+ - [Advanced I/O with Polyphony](docs/advanced-io.md)
80
81
  - [Cheat-Sheet](docs/cheat-sheet.md)
81
82
  - [FAQ](docs/faq.md)
82
83
 
data/TODO.md CHANGED
@@ -1,4 +1,3 @@
1
- - issue #102 - test and see what this is about
2
1
  - Look at RPC benchmark more closely: is there a way to reduce the overhead of
3
2
  the `backend_base_switch_fiber` function?
4
3
 
@@ -15,24 +14,14 @@
15
14
  - Add support for IPv6:
16
15
  https://www.reddit.com/r/ruby/comments/lyen23/understanding_ipv6_and_why_its_important_to_you/
17
16
 
18
- - Check why `throttled_loop` inside of `move_on_after` fails to stop
19
-
20
17
  - Override stock `::SizedQueue` impl with Queue with capacity
21
18
 
22
- - Add support for `break` and `StopIteration` in all loops (with tests)
23
-
24
19
  - More tight loops
25
20
  - `IO#gets_loop`, `Socket#gets_loop`, `OpenSSL::Socket#gets_loop` (medium effort)
26
- - `Fiber#receive_loop` (very little effort, should be implemented in C)
27
21
 
28
22
  - Add support for `close` to io_uring backend
29
23
 
30
- ## Roadmap for Polyphony 1.0
31
-
32
- - Add test that mimics the original design for Monocrono:
33
- - 256 fibers each waiting for a message
34
- - When message received do some blocking work using a `ThreadPool`
35
- - Send messages, collect responses, check for correctness
24
+ ## Roadmap for Polyphony 1.1
36
25
 
37
26
  - io_uring
38
27
  - Use playground.c to find out why we when submitting and waiting for
@@ -117,7 +106,7 @@
117
106
 
118
107
  - Allow locking the scheduler on to one fiber
119
108
  - Add instance var `@fiber_lock`
120
- - API is `Thread#fiber_lock` which sets the fiber_lock instance varwhile
109
+ - API is `Thread#fiber_lock` which sets the fiber_lock instance var while
121
110
  running the block:
122
111
 
123
112
  ```ruby
@@ -127,6 +116,7 @@
127
116
  end
128
117
  end
129
118
  ```
119
+
130
120
  - When `@fiber_lock` is set, it is considered as the only one in the run
131
121
  queue:
132
122
 
@@ -0,0 +1,224 @@
1
+ # @title Advanced I/O with Polyphony
2
+
3
+ ## Using splice for moving data between files and sockets
4
+
5
+ Splice is linux-specific API that lets you move data between two file
6
+ descriptors without copying data between kernel-space and user-space. This is
7
+ not only useful for copying data between two files, but also for implementing
8
+ things such as web servers, where you might need to serve files of an arbitrary
9
+ size. Using splice, you can avoid the cost of having to load a file's content
10
+ into memory, in order to send it to a TCP connection.
11
+
12
+ In order to use `splice`, at least one of the file descriptors involved needs to
13
+ be a pipe. This is because in Linux, pipes are actually kernel buffers. The
14
+ normal way of using splice is that first you splice data from the source fd to
15
+ the pipe (to its *write* fd), and then you splice data from the pipe (from its
16
+ *read* fd) to the destination fd.
17
+
18
+ Here's how we do splicing using Polyphony:
19
+
20
+ ```ruby
21
+ def send_file_using_splice(src, dest)
22
+ # create a pipe. Polyphony::Pipe encapsulates a kernel pipe in a single
23
+ # IO-like object, but we can also use the stock IO.pipe method call that
24
+ # returns two separate pipe fds.
25
+ pipe = Polyphony::Pipe.new
26
+ loop do
27
+ # splices data from src to the pipe
28
+ bytes_spliced = IO.splice(src, pipe, 2**14)
29
+ break if bytes_spliced == 0 # EOF
30
+
31
+ # splices data from the pipe to the dest
32
+ IO.splice(pipe, dest, bytes_spliced)
33
+ end
34
+ end
35
+ ```
36
+
37
+ Let's examine the code above. First of all, we have a loop that repeatedly
38
+ splices data in chunks of 16KB. We break from the loop once EOF is encountered.
39
+ Secondly, on each iteration of the loop we perform two splice operations
40
+ sequentially. So, we need to repeatedly perform two splice operations, one after
41
+ the other. Would there be a better way to do this?
42
+
43
+ Fortunately, Polyphony provides just the tools needed to do that. Firstly, we
44
+ can tell Polyphony to splice data repeatedly until EOF is encountered by passing
45
+ a negative max size:
46
+
47
+ ```ruby
48
+ IO.splice(src, pipe, -2**14)
49
+ ```
50
+
51
+ Secondly, we can perform the two splice operations concurrently, by spinning up
52
+ a separate fiber that performs one of the splice operations, which gives us the
53
+ following:
54
+
55
+ ```ruby
56
+ def send_file_using_splice(src, dest)
57
+ pipe = Polyphony::Pipe.new
58
+ spin do
59
+ IO.splice(src, pipe, -2**14)
60
+ # We need to close the pipe in order to signal EOF for the 2nd splice call.
61
+ pipe.close
62
+ end
63
+ IO.splice(pipe, dest, -2**14)
64
+ end
65
+ ```
66
+
67
+ There are a few things to notice here: While we have two concurrent operations
68
+ running in two separate fibers, their are still inter-dependent in their
69
+ individual progress, as one is filling a kernel buffer, and the other is
70
+ flushing it, and thus the progress of whole will be bound by the slowest
71
+ operation.
72
+
73
+ Imagine an HTTP server that serves a large file to a slow client, or a client
74
+ with a bad network connection. The web server is perfectly capable of reading
75
+ the file from its disk very fast, but sending data to the HTTP client can be
76
+ much much slower. The second splice operation, splicing from the pipe to the
77
+ destination, will flush the kernel much more slowly that it is being filled. At
78
+ a certain point, the buffer is full, and the first splice operation from the
79
+ source to the pipe cannot continue. It will need to wait for the other splice
80
+ operation to progress, in order to continue filling the buffer. This is called
81
+ back-pressure propagation, and we get it automatically.
82
+
83
+ So let's look at all the things we didn't need to do: we didn't need to read
84
+ data into a Ruby string (which is costly in CPU time, in memory, and eventually
85
+ in GC pressure), we didn't need to manage a buffer and take care of
86
+ synchronizing access to the buffer. We got to move data from the source to the
87
+ destination concurrently, and we got back-pressure propagation for free. Can we
88
+ do any better than that?
89
+
90
+ Actually, we can! Polyphony also provides an API that does all of the above in a
91
+ single method call:
92
+
93
+ ```ruby
94
+ def send_file_using_splice(src, dest)
95
+ IO.double_splice(src, dest)
96
+ end
97
+ ```
98
+
99
+ The `IO.double_splice` creates a pipe and repeatedly splices data concurrently
100
+ from the source to pipe and from the pipe to the destination until the source is
101
+ exhausted. All this, without needing to instantiate a `Polyphony::Pipe` object,
102
+ and without needing to spin up a second fiber, further minimizing memory use and
103
+ GC pressure.
104
+
105
+ ## Compressing and decompressing in-flight data
106
+
107
+ You might be familiar with Ruby's [zlib](https://github.com/ruby/zlib) gem (docs
108
+ [here](https://rubyapi.org/3.2/o/zlib)), which can be used to compress and
109
+ uncompress data using the popular gzip format. Imagine we want to implement an
110
+ HTTP server that can serve files compresszed using gzip:
111
+
112
+ ```ruby
113
+ def serve_compressed_file(socket, file)
114
+ # we leave aside sending the HTTP headers and dealing with transfer encoding
115
+ compressed = Zlib.gzip(file.read)
116
+ socket << compressed
117
+ end
118
+ ```
119
+
120
+ In the above example, we have read the file contents into a Ruby string, then
121
+ passed the contents to `Zlib.gzip`, which returned the compressed contents in
122
+ another Ruby string, then wrote the compressed data to the socket. We can see
123
+ how this can lead to large allocations of memory (if the file is large), and
124
+ more pressure on the Ruby GC. How can we improve this?
125
+
126
+ One way would be to utilise Zlib's `GzipWriter` class:
127
+
128
+ ```ruby
129
+ def serve_compressed_file(socket, file)
130
+ # we leave aside sending the HTTP headers and dealing with transfer encoding
131
+ compressor = Zlib::GzipWriter.new(socket)
132
+ while (data = file.read(2**14))
133
+ compressor << data
134
+ end
135
+ end
136
+ ```
137
+
138
+ In the above code, we instantiate a `Zlib::GzipWriter`, which we then feed with
139
+ data from the file, with the compressor object writing the compressed data to
140
+ the socket. Notice how we still need to read the file contents into a Ruby
141
+ string and then pass it to the compressor. Could we avoid this? With Polyphony
142
+ the answer is yes we can!
143
+
144
+ Polyphony provides a number of APIs for compressing and decompressing data on
145
+ the fly between two file descriptors (i.e. `IO` instances), namely: `IO.gzip`,
146
+ `IO.gunzip`, `IO.deflate` and `IO.inflate`. Let's see how this can be used to
147
+ serve gzipped data to an HTTP client:
148
+
149
+ ```ruby
150
+ def serve_compressed_file(socket, file)
151
+ IO.gzip(file, socket) # and that's it!
152
+ end
153
+ ```
154
+
155
+ Using the `IO.gzip` API provided by Polyphony, we completely avoid instantiating
156
+ Ruby strings into which data is read, and in fact we avoid allocating any
157
+ buffers on the heap (apart from what `zlib` might be doing). *And* we get to
158
+ move data *and compress it* between the given file and the socket using a single
159
+ method call!
160
+
161
+ ## Feeding data from a file descriptor to a parser
162
+
163
+ Some times we want to process data from a given file or socket by passing
164
+ through some object that parses the data, or otherwise manipulates it. Normally,
165
+ we would write a loop that repeatedly reads the data from the source, then
166
+ passes it to the parser object. Imagine we have data transmitted using the
167
+ `MessagePack` format that we need to convert back into its original form. We
168
+ might do something like this:
169
+
170
+ ```ruby
171
+ def with_message_pack_data_from_io(io, &block)
172
+ unpacker = MessagePack::Unpacker.new
173
+ while (data = io.read(2**14))
174
+ unpacker.feed_each(data, &block)
175
+ end
176
+ end
177
+
178
+ # Which we can use as follows:
179
+ with_message_pack_data_from_io(socket) do |o|
180
+ puts "got: #{o.inspect}"
181
+ end
182
+ ```
183
+
184
+ Polyphony provides some APIs that help us write less code, and even optimize the
185
+ performance of our code. Let's look at the `IO#read_loop` (or `IO#recv_loop` for
186
+ sockets) API:
187
+
188
+ ```ruby
189
+ def with_message_pack_data_from_io(io, &block)
190
+ unpacker = MessagePack::Unpacker.new
191
+ io.read_loop do |data|
192
+ unpacker.feed_each(data, &block)
193
+ end
194
+ end
195
+ ```
196
+
197
+ In the above code, we replaced our `while` loop with a call to `IO#read_loop`,
198
+ which yields read data to the block given to it. In the block, we pass the data
199
+ to the MessagePack unpacker. While this does not like much different than the
200
+ previous implementation, the `IO#read_loop` API implements a tight loop at the
201
+ C-extension level, that provides slightly better performance.
202
+
203
+ But Polyphony goes even further than that and provides a `IO#feed_loop` API that
204
+ lets us feed read data to a given parser or processor object. Here's how we can
205
+ use it:
206
+
207
+ ```ruby
208
+ def with_message_pack_data_from_io(io, &block)
209
+ unpacker = MessagePack::Unpacker.new
210
+ io.feed_loop(unpacker, :feed_each, &block)
211
+ end
212
+ ```
213
+
214
+ With `IO#feed_loop` we get to write even less code, and as with `IO#read_loop`,
215
+ `IO#feed_loop` is implemented at the C-extension level using a tight loop that
216
+ maximizes performance.
217
+
218
+ ## Conclusion
219
+
220
+ In this article we have looked at some of the advanced I/O functionality
221
+ provided by Polyphony, which lets us write less code, have it run faster, and
222
+ minimize memory allocations and pressure on the Ruby GC. Feel free to browse the
223
+ [IO examples](https://github.com/digital-fabric/polyphony/tree/master/examples/io)
224
+ included in Polyphony.
data/docs/cheat-sheet.md CHANGED
@@ -71,7 +71,7 @@ def calculate_some_stuff(n)
71
71
  acc += big_calc(acc, i)
72
72
  snooze if (i % 1000) == 0
73
73
  end
74
- end
74
+ end
75
75
  ```
76
76
 
77
77
  ### Suspend fiber
@@ -191,7 +191,7 @@ dest2.tee_from(source, 8192)
191
191
  dest1.splice_from(source, 8192)
192
192
  # or:
193
193
  IO.tee(src, dest2)
194
- IO.splice(src, dest2)
194
+ IO.splice(src, dest1)
195
195
  ```
196
196
 
197
197
  ### Splice data between two arbitrary file descriptors, without creating a pipe
data/docs/readme.md CHANGED
@@ -79,6 +79,7 @@ $ gem install polyphony
79
79
 
80
80
  - {file:/docs/overview.md Overview}
81
81
  - {file:/docs/tutorial.md Tutorial}
82
+ - {file:/docs/advanced-io.md Advanced I/O with Polyphony}
82
83
  - {file:/docs/cheat-sheet.md Cheat-Sheet}
83
84
  - {file:/docs/faq.md FAQ}
84
85
 
@@ -94,11 +94,11 @@ def bm_fiber_raw
94
94
  $server_raw.transfer 3
95
95
  end
96
96
 
97
- p bm_raw
98
- p bm_send
99
- p bm_fiber
100
- p bm_fiber_optimized
101
- p bm_fiber_single
97
+ # p bm_raw
98
+ # p bm_send
99
+ # p bm_fiber
100
+ # p bm_fiber_optimized
101
+ # p bm_fiber_single
102
102
  p bm_fiber_raw
103
103
  p bm_fiber_schedule
104
104
 
@@ -116,17 +116,17 @@ end
116
116
 
117
117
  puts "warming up JIT..."
118
118
 
119
- 3.times do
120
- warmup_jit
121
- sleep 1
122
- end
119
+ # 3.times do
120
+ # warmup_jit
121
+ # sleep 1
122
+ # end
123
123
 
124
124
  Benchmark.ips do |x|
125
- x.report("raw") { bm_raw }
126
- x.report("send") { bm_send }
127
- x.report("fiber") { bm_fiber }
128
- x.report("fiber_optimized") { bm_fiber_optimized }
129
- x.report("fiber_single") { bm_fiber_single }
125
+ # x.report("raw") { bm_raw }
126
+ # x.report("send") { bm_send }
127
+ # x.report("fiber") { bm_fiber }
128
+ # x.report("fiber_optimized") { bm_fiber_optimized }
129
+ # x.report("fiber_single") { bm_fiber_single }
130
130
  x.report("fiber_raw") { bm_fiber_raw }
131
131
  x.report("fiber_schedule") { bm_fiber_schedule }
132
132
  x.compare!
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+
6
+ class Stream
7
+ def initialize(io)
8
+ @io = io
9
+ @buffer = +''
10
+ @length = 0
11
+ @pos = 0
12
+ end
13
+
14
+ def getbyte
15
+ if @pos == @length
16
+ return nil if !fill_buffer
17
+ end
18
+ byte = @buffer[@pos].getbyte(0)
19
+ @pos += 1
20
+ byte
21
+ end
22
+
23
+ def getc
24
+ if @pos == @length
25
+ return nil if !fill_buffer
26
+ end
27
+ char = @buffer[@pos]
28
+ @pos += 1
29
+ char
30
+ end
31
+
32
+ def ungetc(c)
33
+ @buffer.insert(@pos, c)
34
+ @length += 1
35
+ c
36
+ end
37
+
38
+ def gets
39
+ end
40
+
41
+ def read
42
+ end
43
+
44
+ def readpartial
45
+ end
46
+
47
+ private
48
+
49
+ def fill_buffer
50
+ Polyphony.backend_read(@io, @buffer, 8192, false, -1)
51
+ @length = @buffer.size
52
+ end
53
+ end
54
+
55
+ i, o = IO.pipe
56
+ s = Stream.new(i)
57
+
58
+ f = spin do
59
+ loop do
60
+ b = s.getbyte
61
+ p getbyte: b
62
+ s.ungetc(b.to_s) if rand > 0.5
63
+ end
64
+ end
65
+
66
+ o << 'hello'
67
+ sleep 0.1
68
+
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'polyphony'
5
+
6
+ i = 0
7
+ value = move_on_after(1, with_value: 42) do
8
+ throttled_loop(20) do
9
+ p (i += 1)
10
+ end
11
+ end
12
+
13
+ p value: value
@@ -389,12 +389,12 @@ inline void set_fd_blocking_mode(int fd, int blocking) {
389
389
  #endif
390
390
  }
391
391
 
392
- inline void io_verify_blocking_mode(rb_io_t *fptr, VALUE io, VALUE blocking) {
392
+ inline void io_verify_blocking_mode(VALUE io, int fd, VALUE blocking) {
393
393
  VALUE blocking_mode = rb_ivar_get(io, ID_ivar_blocking_mode);
394
394
  if (blocking == blocking_mode) return;
395
395
 
396
396
  rb_ivar_set(io, ID_ivar_blocking_mode, blocking);
397
- set_fd_blocking_mode(fptr->fd, blocking == Qtrue);
397
+ set_fd_blocking_mode(fd, blocking == Qtrue);
398
398
  }
399
399
 
400
400
  inline void backend_run_idle_tasks(struct Backend_base *base) {
@@ -455,9 +455,7 @@ VALUE Backend_stats(VALUE self) {
455
455
  }
456
456
 
457
457
  VALUE Backend_verify_blocking_mode(VALUE self, VALUE io, VALUE blocking) {
458
- rb_io_t *fptr;
459
- GetOpenFile(io, fptr);
460
- io_verify_blocking_mode(fptr, io, blocking);
458
+ io_verify_blocking_mode(io, rb_io_descriptor(io), blocking);
461
459
  return self;
462
460
  }
463
461
 
@@ -10,6 +10,15 @@
10
10
  #include "ruby/io.h"
11
11
  #include "runqueue.h"
12
12
 
13
+ #ifndef HAVE_RB_IO_DESCRIPTOR
14
+ static int rb_io_descriptor_fallback(VALUE io) {
15
+ rb_io_t *fptr;
16
+ GetOpenFile(io, fptr);
17
+ return fptr->fd;
18
+ }
19
+ #define rb_io_descriptor rb_io_descriptor_fallback
20
+ #endif
21
+
13
22
  struct backend_stats {
14
23
  unsigned int runqueue_size;
15
24
  unsigned int runqueue_length;
@@ -145,7 +154,7 @@ VALUE Backend_stats(VALUE self);
145
154
  VALUE Backend_verify_blocking_mode(VALUE self, VALUE io, VALUE blocking);
146
155
  void backend_run_idle_tasks(struct Backend_base *base);
147
156
  void set_fd_blocking_mode(int fd, int blocking);
148
- void io_verify_blocking_mode(rb_io_t *fptr, VALUE io, VALUE blocking);
157
+ void io_verify_blocking_mode(VALUE io, int fd, VALUE blocking);
149
158
  void backend_setup_stats_symbols();
150
159
  int backend_getaddrinfo(VALUE host, VALUE port, struct sockaddr **ai_addr);
151
160
  VALUE name_to_addrinfo(void *name, socklen_t len);
@@ -28,9 +28,9 @@ VALUE SYM_write;
28
28
  VALUE eArgumentError;
29
29
 
30
30
  #ifdef POLYPHONY_UNSET_NONBLOCK
31
- #define io_unset_nonblock(fptr, io) io_verify_blocking_mode(fptr, io, Qtrue)
31
+ #define io_unset_nonblock(io, fd) io_verify_blocking_mode(io, fd, Qtrue)
32
32
  #else
33
- #define io_unset_nonblock(fptr, io)
33
+ #define io_unset_nonblock(io, fd)
34
34
  #endif
35
35
 
36
36
  typedef struct Backend_t {
@@ -389,10 +389,10 @@ static inline int fd_from_io(VALUE io, rb_io_t **fptr, int write_mode, int recti
389
389
  if (underlying_io != Qnil) io = underlying_io;
390
390
 
391
391
  GetOpenFile(io, *fptr);
392
- io_unset_nonblock(*fptr, io);
392
+ int fd = rb_io_descriptor(io);
393
+ io_unset_nonblock(io, fd);
393
394
  if (rectify_file_pos) rectify_io_file_pos(*fptr);
394
-
395
- return (*fptr)->fd;
395
+ return fd;
396
396
  }
397
397
  }
398
398
 
@@ -1376,7 +1376,7 @@ VALUE Backend_wait_io(VALUE self, VALUE io, VALUE write) {
1376
1376
 
1377
1377
  // if (fd < 0) return Qnil;
1378
1378
 
1379
- // io_unset_nonblock(fptr, io);
1379
+ // io_unset_nonblock(io, fd);
1380
1380
 
1381
1381
  // ctx = context_store_acquire(&backend->store, OP_CLOSE);
1382
1382
  // sqe = io_uring_backend_get_sqe(backend);
@@ -277,10 +277,10 @@ static inline int fd_from_io(VALUE io, rb_io_t **fptr, int write_mode, int recti
277
277
  if (underlying_io != Qnil) io = underlying_io;
278
278
 
279
279
  GetOpenFile(io, *fptr);
280
- io_verify_blocking_mode(*fptr, io, Qfalse);
280
+ int fd = rb_io_descriptor(io);
281
+ io_verify_blocking_mode(io, fd, Qfalse);
281
282
  if (rectify_file_pos) rectify_io_file_pos(*fptr);
282
-
283
- return (*fptr)->fd;
283
+ return fd;
284
284
  }
285
285
  }
286
286
 
@@ -681,7 +681,7 @@ VALUE Backend_accept(VALUE self, VALUE server_socket, VALUE socket_class) {
681
681
  fp->fd = fd;
682
682
  fp->mode = FMODE_READWRITE | FMODE_DUPLEX;
683
683
  rb_io_ascii8bit_binmode(socket);
684
- io_verify_blocking_mode(fp, socket, Qfalse);
684
+ io_verify_blocking_mode(socket, fd, Qfalse);
685
685
  rb_io_synchronized(fp);
686
686
 
687
687
  // if (rsock_do_not_reverse_lookup) {
@@ -736,7 +736,7 @@ VALUE Backend_accept_loop(VALUE self, VALUE server_socket, VALUE socket_class) {
736
736
  fp->fd = fd;
737
737
  fp->mode = FMODE_READWRITE | FMODE_DUPLEX;
738
738
  rb_io_ascii8bit_binmode(socket);
739
- io_verify_blocking_mode(fp, socket, Qfalse);
739
+ io_verify_blocking_mode(socket, fd, Qfalse);
740
740
  rb_io_synchronized(fp);
741
741
 
742
742
  rb_yield(socket);
@@ -98,4 +98,10 @@ end
98
98
 
99
99
  have_header('ruby/io/buffer.h')
100
100
 
101
+ have_func('rb_io_path')
102
+ have_func('rb_io_descriptor')
103
+ have_func('rb_io_get_write_io')
104
+ have_func('rb_io_closed_p')
105
+ have_func('rb_io_open_descriptor')
106
+
101
107
  create_makefile 'polyphony_ext'
@@ -1,3 +1,4 @@
1
+ #include <stdnoreturn.h>
1
2
  #include "polyphony.h"
2
3
 
3
4
  ID ID_ivar_auto_watcher;
@@ -129,7 +130,7 @@ VALUE Fiber_send(VALUE self, VALUE msg) {
129
130
  return self;
130
131
  }
131
132
 
132
- /* Receive's a message from the fiber's mailbox. If no message is available,
133
+ /* Receives a message from the fiber's mailbox. If no message is available,
133
134
  * waits for a message to be sent to it.
134
135
  *
135
136
  * @return [any] received message
@@ -144,6 +145,24 @@ VALUE Fiber_receive(VALUE self) {
144
145
  return Queue_shift(0, 0, mailbox);
145
146
  }
146
147
 
148
+ /* Receives messages from the fiber's mailbox in an infinite loop.
149
+ *
150
+ */
151
+
152
+ noreturn VALUE Fiber_receive_loop(VALUE self) {
153
+ VALUE mailbox = rb_ivar_get(self, ID_ivar_mailbox);
154
+ if (mailbox == Qnil) {
155
+ mailbox = rb_funcall(cQueue, ID_new, 0);
156
+ rb_ivar_set(self, ID_ivar_mailbox, mailbox);
157
+ }
158
+
159
+ while (1) {
160
+ VALUE msg = Queue_shift(0, 0,mailbox);
161
+ rb_yield(msg);
162
+ RB_GC_GUARD(msg);
163
+ }
164
+ }
165
+
147
166
  /* Returns the fiber's mailbox.
148
167
  *
149
168
  * @return [Queue]
@@ -201,6 +220,7 @@ void Init_Fiber(void) {
201
220
  rb_define_method(cFiber, "<<", Fiber_send, 1);
202
221
  rb_define_method(cFiber, "send", Fiber_send, 1);
203
222
  rb_define_method(cFiber, "receive", Fiber_receive, 0);
223
+ rb_define_method(cFiber, "receive_loop", Fiber_receive_loop, 0);
204
224
  rb_define_method(cFiber, "receive_all_pending", Fiber_receive_all_pending, 0);
205
225
  rb_define_method(cFiber, "mailbox", Fiber_mailbox, 0);
206
226