rage-iodine 1.7.58

Sign up to get free protection for your applications and to get access to all the features.
Files changed (128) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
  3. data/.github/workflows/ruby.yml +42 -0
  4. data/.gitignore +20 -0
  5. data/.rspec +2 -0
  6. data/.yardopts +8 -0
  7. data/CHANGELOG.md +1098 -0
  8. data/Gemfile +11 -0
  9. data/LICENSE.txt +21 -0
  10. data/LIMITS.md +41 -0
  11. data/README.md +782 -0
  12. data/Rakefile +23 -0
  13. data/SPEC-PubSub-Draft.md +159 -0
  14. data/SPEC-WebSocket-Draft.md +239 -0
  15. data/bin/console +22 -0
  16. data/bin/info.md +353 -0
  17. data/bin/mustache_bench.rb +100 -0
  18. data/bin/poc/Gemfile.lock +23 -0
  19. data/bin/poc/README.md +37 -0
  20. data/bin/poc/config.ru +66 -0
  21. data/bin/poc/gemfile +1 -0
  22. data/bin/poc/www/index.html +57 -0
  23. data/examples/async_task.ru +92 -0
  24. data/examples/bates/README.md +3 -0
  25. data/examples/bates/config.ru +342 -0
  26. data/examples/bates/david+bold.pdf +0 -0
  27. data/examples/bates/public/drop-pdf.png +0 -0
  28. data/examples/bates/public/index.html +600 -0
  29. data/examples/config.ru +59 -0
  30. data/examples/echo.ru +59 -0
  31. data/examples/etag.ru +16 -0
  32. data/examples/hello.ru +29 -0
  33. data/examples/pubsub_engine.ru +81 -0
  34. data/examples/rack3.ru +12 -0
  35. data/examples/redis.ru +70 -0
  36. data/examples/shootout.ru +73 -0
  37. data/examples/sub-protocols.ru +90 -0
  38. data/examples/tcp_client.rb +66 -0
  39. data/examples/x-sendfile.ru +14 -0
  40. data/exe/iodine +280 -0
  41. data/ext/iodine/extconf.rb +110 -0
  42. data/ext/iodine/fio.c +12096 -0
  43. data/ext/iodine/fio.h +6390 -0
  44. data/ext/iodine/fio_cli.c +431 -0
  45. data/ext/iodine/fio_cli.h +189 -0
  46. data/ext/iodine/fio_json_parser.h +687 -0
  47. data/ext/iodine/fio_siphash.c +157 -0
  48. data/ext/iodine/fio_siphash.h +37 -0
  49. data/ext/iodine/fio_tls.h +129 -0
  50. data/ext/iodine/fio_tls_missing.c +649 -0
  51. data/ext/iodine/fio_tls_openssl.c +1056 -0
  52. data/ext/iodine/fio_tmpfile.h +50 -0
  53. data/ext/iodine/fiobj.h +44 -0
  54. data/ext/iodine/fiobj4fio.h +21 -0
  55. data/ext/iodine/fiobj_ary.c +333 -0
  56. data/ext/iodine/fiobj_ary.h +139 -0
  57. data/ext/iodine/fiobj_data.c +1185 -0
  58. data/ext/iodine/fiobj_data.h +167 -0
  59. data/ext/iodine/fiobj_hash.c +409 -0
  60. data/ext/iodine/fiobj_hash.h +176 -0
  61. data/ext/iodine/fiobj_json.c +622 -0
  62. data/ext/iodine/fiobj_json.h +68 -0
  63. data/ext/iodine/fiobj_mem.h +71 -0
  64. data/ext/iodine/fiobj_mustache.c +317 -0
  65. data/ext/iodine/fiobj_mustache.h +62 -0
  66. data/ext/iodine/fiobj_numbers.c +344 -0
  67. data/ext/iodine/fiobj_numbers.h +127 -0
  68. data/ext/iodine/fiobj_str.c +433 -0
  69. data/ext/iodine/fiobj_str.h +172 -0
  70. data/ext/iodine/fiobject.c +620 -0
  71. data/ext/iodine/fiobject.h +654 -0
  72. data/ext/iodine/hpack.h +1923 -0
  73. data/ext/iodine/http.c +2736 -0
  74. data/ext/iodine/http.h +1019 -0
  75. data/ext/iodine/http1.c +825 -0
  76. data/ext/iodine/http1.h +29 -0
  77. data/ext/iodine/http1_parser.h +1835 -0
  78. data/ext/iodine/http_internal.c +1279 -0
  79. data/ext/iodine/http_internal.h +248 -0
  80. data/ext/iodine/http_mime_parser.h +350 -0
  81. data/ext/iodine/iodine.c +1433 -0
  82. data/ext/iodine/iodine.h +64 -0
  83. data/ext/iodine/iodine_caller.c +218 -0
  84. data/ext/iodine/iodine_caller.h +27 -0
  85. data/ext/iodine/iodine_connection.c +941 -0
  86. data/ext/iodine/iodine_connection.h +55 -0
  87. data/ext/iodine/iodine_defer.c +420 -0
  88. data/ext/iodine/iodine_defer.h +6 -0
  89. data/ext/iodine/iodine_fiobj2rb.h +120 -0
  90. data/ext/iodine/iodine_helpers.c +282 -0
  91. data/ext/iodine/iodine_helpers.h +12 -0
  92. data/ext/iodine/iodine_http.c +1280 -0
  93. data/ext/iodine/iodine_http.h +23 -0
  94. data/ext/iodine/iodine_json.c +302 -0
  95. data/ext/iodine/iodine_json.h +6 -0
  96. data/ext/iodine/iodine_mustache.c +567 -0
  97. data/ext/iodine/iodine_mustache.h +6 -0
  98. data/ext/iodine/iodine_pubsub.c +580 -0
  99. data/ext/iodine/iodine_pubsub.h +26 -0
  100. data/ext/iodine/iodine_rack_io.c +273 -0
  101. data/ext/iodine/iodine_rack_io.h +20 -0
  102. data/ext/iodine/iodine_store.c +142 -0
  103. data/ext/iodine/iodine_store.h +20 -0
  104. data/ext/iodine/iodine_tcp.c +346 -0
  105. data/ext/iodine/iodine_tcp.h +13 -0
  106. data/ext/iodine/iodine_tls.c +261 -0
  107. data/ext/iodine/iodine_tls.h +13 -0
  108. data/ext/iodine/mustache_parser.h +1546 -0
  109. data/ext/iodine/redis_engine.c +957 -0
  110. data/ext/iodine/redis_engine.h +79 -0
  111. data/ext/iodine/resp_parser.h +317 -0
  112. data/ext/iodine/scheduler.c +173 -0
  113. data/ext/iodine/scheduler.h +6 -0
  114. data/ext/iodine/websocket_parser.h +506 -0
  115. data/ext/iodine/websockets.c +752 -0
  116. data/ext/iodine/websockets.h +185 -0
  117. data/iodine.gemspec +50 -0
  118. data/lib/iodine/connection.rb +61 -0
  119. data/lib/iodine/json.rb +42 -0
  120. data/lib/iodine/mustache.rb +113 -0
  121. data/lib/iodine/pubsub.rb +55 -0
  122. data/lib/iodine/rack_utils.rb +43 -0
  123. data/lib/iodine/tls.rb +16 -0
  124. data/lib/iodine/version.rb +3 -0
  125. data/lib/iodine.rb +274 -0
  126. data/lib/rack/handler/iodine.rb +33 -0
  127. data/logo.png +0 -0
  128. metadata +284 -0
@@ -0,0 +1,941 @@
1
+ #include "iodine_connection.h"
2
+
3
+ #define FIO_INCLUDE_LINKED_LIST
4
+ #define FIO_INCLUDE_STR
5
+ #include "fio.h"
6
+
7
+ #include "fiobj4fio.h"
8
+ #include "websockets.h"
9
+
10
+ #include <ruby/io.h>
11
+
12
+ /* *****************************************************************************
13
+ Constants in use
14
+ ***************************************************************************** */
15
+
16
+ static ID new_id;
17
+ static ID call_id;
18
+ static ID to_id;
19
+ static ID channel_id;
20
+ static ID as_id;
21
+ static ID binary_id;
22
+ static ID match_id;
23
+ static ID redis_id;
24
+ static ID handler_id;
25
+ static ID engine_id;
26
+ static ID message_id;
27
+ static ID on_open_id;
28
+ static ID on_message_id;
29
+ static ID on_drained_id;
30
+ static ID ping_id;
31
+ static ID on_shutdown_id;
32
+ static ID on_close_id;
33
+ static VALUE ConnectionKlass;
34
+ static rb_encoding *IodineUTF8Encoding;
35
+ static VALUE WebSocketSymbol;
36
+ static VALUE SSESymbol;
37
+ static VALUE RAWSymbol;
38
+
39
+ /* *****************************************************************************
40
+ Pub/Sub storage
41
+ ***************************************************************************** */
42
+
43
+ #define FIO_SET_NAME fio_subhash
44
+ #define FIO_SET_OBJ_TYPE subscription_s *
45
+ #define FIO_SET_KEY_TYPE fio_str_info_s
46
+ #define FIO_SET_KEY_COMPARE(s1, s2) \
47
+ ((s1).len == (s2).len && \
48
+ ((s1).data == (s2).data || !memcmp((s1).data, (s2).data, (s1).len)))
49
+ #define FIO_SET_OBJ_DESTROY(obj) fio_unsubscribe((obj))
50
+ #include <fio.h> // creates the fio_str_set_s Set and functions
51
+
52
+ static inline VALUE iodine_sub_unsubscribe(fio_subhash_s *store,
53
+ fio_str_info_s channel) {
54
+ if (fio_subhash_remove(store, fiobj_hash_string(channel.data, channel.len),
55
+ channel, NULL))
56
+ return Qfalse;
57
+ return Qtrue;
58
+ }
59
+ static inline void iodine_sub_add(fio_subhash_s *store, subscription_s *sub) {
60
+ fio_str_info_s ch = fio_subscription_channel(sub);
61
+ fio_subhash_insert(store, fiobj_hash_string(ch.data, ch.len), ch, sub, NULL);
62
+ }
63
+ static inline void iodine_sub_clear_all(fio_subhash_s *store) {
64
+ fio_subhash_free(store);
65
+ }
66
+
67
+ static fio_lock_i sub_lock = FIO_LOCK_INIT;
68
+ static fio_subhash_s sub_global = FIO_SET_INIT;
69
+
70
+ /* *****************************************************************************
71
+ C <=> Ruby Data allocation
72
+ ***************************************************************************** */
73
+
74
+ typedef struct {
75
+ iodine_connection_s info;
76
+ size_t ref;
77
+ fio_subhash_s subscriptions;
78
+ fio_lock_i lock;
79
+ uint8_t answers_on_message;
80
+ uint8_t answers_on_drained;
81
+ uint8_t answers_ping;
82
+ /* these are one-shot, but the CPU cache might have the data, so set it */
83
+ uint8_t answers_on_open;
84
+ uint8_t answers_on_shutdown;
85
+ uint8_t answers_on_close;
86
+ } iodine_connection_data_s;
87
+
88
+ /** frees an iodine_connection_data_s object*/
89
+
90
+ /* a callback for the GC (marking active objects) */
91
+ static void iodine_connection_data_mark(void *c_) {
92
+ iodine_connection_data_s *c = c_;
93
+ if (!c) {
94
+ return;
95
+ }
96
+ if (c->info.handler && c->info.handler != Qnil) {
97
+ rb_gc_mark(c->info.handler);
98
+ }
99
+ if (c->info.env && c->info.env != Qnil) {
100
+ rb_gc_mark(c->info.env);
101
+ }
102
+ }
103
+ /* a callback for the GC (freeing inactive objects) */
104
+ static void iodine_connection_data_free(void *c_) {
105
+ iodine_connection_data_s *data = c_;
106
+ if (fio_atomic_sub(&data->ref, 1))
107
+ return;
108
+ free(data);
109
+ }
110
+
111
+ static size_t iodine_connection_data_size(const void *c_) {
112
+ return sizeof(iodine_connection_data_s);
113
+ (void)c_;
114
+ }
115
+
116
+ const rb_data_type_t iodine_connection_data_type = {
117
+ .wrap_struct_name = "IodineConnectionData",
118
+ .function =
119
+ {
120
+ .dmark = iodine_connection_data_mark,
121
+ .dfree = iodine_connection_data_free,
122
+ .dsize = iodine_connection_data_size,
123
+ },
124
+ .data = NULL,
125
+ // .flags = RUBY_TYPED_FREE_IMMEDIATELY,
126
+ };
127
+
128
+ /* Iodine::PubSub::Engine.allocate */
129
+ static VALUE iodine_connection_data_alloc_c(VALUE self) {
130
+ iodine_connection_data_s *c = malloc(sizeof(*c));
131
+ *c = (iodine_connection_data_s){
132
+ .info.handler = (VALUE)0,
133
+ .info.uuid = -1,
134
+ .ref = 1,
135
+ .subscriptions = FIO_SET_INIT,
136
+ .lock = FIO_LOCK_INIT,
137
+ };
138
+ return TypedData_Wrap_Struct(self, &iodine_connection_data_type, c);
139
+ }
140
+
141
+ static inline iodine_connection_data_s *iodine_connection_ruby2C(VALUE self) {
142
+ iodine_connection_data_s *c = NULL;
143
+ TypedData_Get_Struct(self, iodine_connection_data_s,
144
+ &iodine_connection_data_type, c);
145
+ return c;
146
+ }
147
+
148
+ static inline iodine_connection_data_s *
149
+ iodine_connection_validate_data(VALUE self) {
150
+ iodine_connection_data_s *c = iodine_connection_ruby2C(self);
151
+ if (c == NULL || c->info.handler == Qnil || c->info.uuid == -1) {
152
+ return NULL;
153
+ }
154
+ return c;
155
+ }
156
+
157
+ /* *****************************************************************************
158
+ Ruby Connection Methods - write, close open? pending
159
+ ***************************************************************************** */
160
+
161
+ /**
162
+ * Writes data to the connection asynchronously. `data` MUST be a String.
163
+ *
164
+ * In effect, the `write` call does nothing, it only schedules the data to be
165
+ * sent and marks the data as pending.
166
+ *
167
+ * Use {pending} to test how many `write` operations are pending completion
168
+ * (`on_drained(client)` will be called when they complete).
169
+ */
170
+ static VALUE iodine_connection_write(VALUE self, VALUE data) {
171
+ iodine_connection_data_s *c = iodine_connection_validate_data(self);
172
+ if (!c || fio_is_closed(c->info.uuid)) {
173
+ // don't throw exceptions - closed connections are unavoidable.
174
+ return Qnil;
175
+ // rb_raise(rb_eIOError, "Connection closed or invalid.");
176
+ }
177
+ if (!RB_TYPE_P(data, T_STRING)) {
178
+ VALUE tmp = data;
179
+ data = IodineCaller.call(data, iodine_to_s_id);
180
+ if (!RB_TYPE_P(data, T_STRING))
181
+ Check_Type(tmp, T_STRING);
182
+ rb_backtrace();
183
+ FIO_LOG_WARNING(
184
+ "`Iodine::Connection#write` was called with a non-String object.");
185
+ }
186
+
187
+ switch (c->info.type) {
188
+ case IODINE_CONNECTION_WEBSOCKET:
189
+ /* WebSockets*/
190
+ websocket_write(c->info.arg, IODINE_RSTRINFO(data),
191
+ rb_enc_get(data) == IodineUTF8Encoding);
192
+ return Qtrue;
193
+ break;
194
+ case IODINE_CONNECTION_SSE:
195
+ /* SSE */
196
+ #if 1
197
+ http_sse_write(c->info.arg, .data = IODINE_RSTRINFO(data));
198
+ return Qtrue;
199
+ #else
200
+ if (rb_enc_get(data) == IodineUTF8Encoding) {
201
+ http_sse_write(c->info.arg, .data = {.data = RSTRING_PTR(data),
202
+ .len = RSTRING_LEN(data)});
203
+ return Qtrue;
204
+ }
205
+ fprintf(stderr, "WARNING: ignoring an attept to write binary data to "
206
+ "non-binary protocol (SSE).\n");
207
+ return Qfalse;
208
+ // rb_raise(rb_eEncodingError,
209
+ // "This Connection type requires data to be UTF-8 encoded.");
210
+ #endif
211
+ break;
212
+ case IODINE_CONNECTION_RAW: /* fallthrough */
213
+ default: {
214
+ fio_write(c->info.uuid, RSTRING_PTR(data), RSTRING_LEN(data));
215
+ return Qtrue;
216
+ } break;
217
+ }
218
+ return Qnil;
219
+ }
220
+
221
+ /**
222
+ * Schedules the connection to be closed.
223
+ *
224
+ * The connection will be closed once all the scheduled `write` operations have
225
+ * been completed (or failed).
226
+ */
227
+ static VALUE iodine_connection_close(VALUE self) {
228
+ iodine_connection_data_s *c = iodine_connection_validate_data(self);
229
+ if (c && !fio_is_closed(c->info.uuid)) {
230
+ if (c->info.type == IODINE_CONNECTION_WEBSOCKET) {
231
+ websocket_close(c->info.arg); /* sends WebSocket close packet */
232
+ } else {
233
+ fio_close(c->info.uuid);
234
+ }
235
+ }
236
+
237
+ return Qnil;
238
+ }
239
+ /** Returns true if the connection appears to be open (no known issues). */
240
+ static VALUE iodine_connection_is_open(VALUE self) {
241
+ iodine_connection_data_s *c = iodine_connection_validate_data(self);
242
+ if (c && !fio_is_closed(c->info.uuid)) {
243
+ return Qtrue;
244
+ }
245
+ return Qfalse;
246
+ }
247
+
248
+ /**
249
+ * Always returns true, since Iodine connections support the pub/sub extension.
250
+ */
251
+ static VALUE iodine_connection_is_pubsub(VALUE self) {
252
+ return INT2NUM(0);
253
+ (void)self;
254
+ }
255
+ /**
256
+ * Returns the number of pending `write` operations that need to complete
257
+ * before the next `on_drained` callback is called.
258
+ *
259
+ * Returns -1 if the connection is closed and 0 if `on_drained` won't be
260
+ * scheduled (no pending `write`).
261
+ */
262
+ static VALUE iodine_connection_pending(VALUE self) {
263
+ iodine_connection_data_s *c = iodine_connection_validate_data(self);
264
+ if (!c || fio_is_closed(c->info.uuid)) {
265
+ return INT2NUM(-1);
266
+ }
267
+ return SIZET2NUM((fio_pending(c->info.uuid)));
268
+ }
269
+
270
+ // clang-format off
271
+ /**
272
+ * Returns the connection's protocol Symbol (`:sse`, `:websocket` or `:raw`).
273
+ *
274
+ * @Note For compatibility reasons (with ther `rack.upgrade` servers), it might be more prudent to use the data in the {#env} (`env['rack.upgrade?']`). However, this method is provided both as a faster alternative and for those cases where Raw / Custom (TCP/IP) data stream is a valid option.
275
+ */
276
+ static VALUE iodine_connection_protocol_name(VALUE self) {
277
+ // clang-format on
278
+ iodine_connection_data_s *c = iodine_connection_validate_data(self);
279
+ if (c) {
280
+ switch (c->info.type) {
281
+ case IODINE_CONNECTION_WEBSOCKET:
282
+ return WebSocketSymbol;
283
+ break;
284
+ case IODINE_CONNECTION_SSE:
285
+ return SSESymbol;
286
+ break;
287
+ case IODINE_CONNECTION_RAW: /* fallthrough */
288
+ return RAWSymbol;
289
+ break;
290
+ }
291
+ }
292
+ return Qnil;
293
+ }
294
+
295
+ /**
296
+ * Returns the timeout / `ping` interval for the connection.
297
+ *
298
+ * Returns nil on error.
299
+ */
300
+ static VALUE iodine_connection_timeout_get(VALUE self) {
301
+ iodine_connection_data_s *c = iodine_connection_validate_data(self);
302
+ if (c && !fio_is_closed(c->info.uuid)) {
303
+ size_t tout = (size_t)fio_timeout_get(c->info.uuid);
304
+ return SIZET2NUM(tout);
305
+ }
306
+ return Qnil;
307
+ }
308
+
309
+ /**
310
+ * Sets the timeout / `ping` interval for the connection (up to 255 seconds).
311
+ *
312
+ * Returns nil on error.
313
+ */
314
+ static VALUE iodine_connection_timeout_set(VALUE self, VALUE timeout) {
315
+ Check_Type(timeout, T_FIXNUM);
316
+ int tout = NUM2INT(timeout);
317
+ if (tout < 0 || tout > 255) {
318
+ rb_raise(rb_eRangeError, "timeout out of range.");
319
+ return Qnil;
320
+ }
321
+ iodine_connection_data_s *c = iodine_connection_validate_data(self);
322
+ if (c && !fio_is_closed(c->info.uuid)) {
323
+ fio_timeout_set(c->info.uuid, (uint8_t)tout);
324
+ return timeout;
325
+ }
326
+ return Qnil;
327
+ }
328
+
329
+ /**
330
+ * Returns the connection's `env` (if it originated from an HTTP request).
331
+ */
332
+ static VALUE iodine_connection_env(VALUE self) {
333
+ iodine_connection_data_s *c = iodine_connection_validate_data(self);
334
+ if (c && c->info.env) {
335
+ return c->info.env;
336
+ }
337
+ return Qnil;
338
+ }
339
+
340
+ /**
341
+ * Returns the client's current callback object.
342
+ */
343
+ static VALUE iodine_connection_handler_get(VALUE self) {
344
+ iodine_connection_data_s *data = iodine_connection_validate_data(self);
345
+ if (!data) {
346
+ FIO_LOG_DEBUG("(iodine) requested connection handler for "
347
+ "an invalid connection: %p",
348
+ (void *)self);
349
+ return Qnil;
350
+ }
351
+ return data->info.handler;
352
+ }
353
+
354
+ // clang-format off
355
+ /**
356
+ * Sets the client's callback object, so future events will use the new object's callbacks.
357
+ *
358
+ * @Note this will fire the `on_close` callback in the old handler and the `on_open` callback on the new handler. However, existing subscriptions will remain intact.
359
+ */
360
+ static VALUE iodine_connection_handler_set(VALUE self, VALUE handler) {
361
+ // clang-format on
362
+ iodine_connection_data_s *data = iodine_connection_validate_data(self);
363
+ if (!data) {
364
+ FIO_LOG_DEBUG("(iodine) attempted to set a connection handler for "
365
+ "an invalid connection: %p",
366
+ (void *)self);
367
+ return Qnil;
368
+ }
369
+ if (handler == Qnil || handler == Qfalse) {
370
+ FIO_LOG_DEBUG(
371
+ "(iodine) called client.handler = nil, closing connection: %p",
372
+ (void *)self);
373
+ iodine_connection_close(self);
374
+ return Qnil;
375
+ }
376
+ if (data->info.handler != handler) {
377
+ uint8_t answers_on_open = (rb_respond_to(handler, on_open_id) != 0);
378
+ if (data->answers_on_close)
379
+ IodineCaller.call2(data->info.handler, on_close_id, 1, &self);
380
+ fio_lock(&data->lock);
381
+ data->info.handler = handler;
382
+ data->answers_on_open = answers_on_open,
383
+ data->answers_on_message = (rb_respond_to(handler, on_message_id) != 0),
384
+ data->answers_ping = (rb_respond_to(handler, ping_id) != 0),
385
+ data->answers_on_drained = (rb_respond_to(handler, on_drained_id) != 0),
386
+ data->answers_on_shutdown = (rb_respond_to(handler, on_shutdown_id) != 0),
387
+ data->answers_on_close = (rb_respond_to(handler, on_close_id) != 0),
388
+ fio_unlock(&data->lock);
389
+ if (answers_on_open) {
390
+ iodine_connection_fire_event(self, IODINE_CONNECTION_ON_OPEN, Qnil);
391
+ }
392
+ FIO_LOG_DEBUG("(iodine) switched handlers for connection: %p",
393
+ (void *)self);
394
+ }
395
+ return handler;
396
+ }
397
+ /* *****************************************************************************
398
+ Pub/Sub Callbacks (internal implementation)
399
+ ***************************************************************************** */
400
+
401
+ /* calls the Ruby block assigned to a pubsub event (within the GVL). */
402
+ static void *iodine_on_pubsub_call_block(void *msg_) {
403
+ fio_msg_s *msg = msg_;
404
+ VALUE args[2];
405
+ args[0] = rb_str_new(msg->channel.data, msg->channel.len);
406
+ IodineStore.add(args[0]);
407
+ args[1] = rb_str_new(msg->msg.data, msg->msg.len);
408
+ IodineStore.add(args[1]);
409
+ IodineCaller.call2((VALUE)msg->udata2, call_id, 2, args);
410
+ IodineStore.remove(args[1]);
411
+ IodineStore.remove(args[0]);
412
+ return NULL;
413
+ }
414
+
415
+ /* callback for incoming subscription messages */
416
+ static void iodine_on_pubsub(fio_msg_s *msg) {
417
+ iodine_connection_data_s *data = msg->udata1;
418
+ VALUE block = (VALUE)msg->udata2;
419
+ switch (block) {
420
+ case 0: /* fallthrough */
421
+ case Qnil: /* fallthrough */
422
+ case Qtrue: { /* Qtrue == binary WebSocket */
423
+ if (!data) {
424
+ FIO_LOG_ERROR("Pub/Sub direct called with no connection data!");
425
+ return;
426
+ }
427
+ if (data->info.handler == Qnil || data->info.uuid == -1 ||
428
+ fio_is_closed(data->info.uuid))
429
+ return;
430
+ switch (data->info.type) {
431
+ case IODINE_CONNECTION_WEBSOCKET: {
432
+ FIOBJ s = (FIOBJ)fio_message_metadata(
433
+ msg, (block == Qnil ? WEBSOCKET_OPTIMIZE_PUBSUB
434
+ : WEBSOCKET_OPTIMIZE_PUBSUB_BINARY));
435
+ if (s) {
436
+ // fwrite(".", 1, 1, stderr);
437
+ fiobj_send_free(data->info.uuid, fiobj_dup(s));
438
+ } else {
439
+ fwrite("-", 1, 1, stderr);
440
+ websocket_write(data->info.arg, msg->msg, (block == Qnil));
441
+ }
442
+ return;
443
+ }
444
+ case IODINE_CONNECTION_SSE:
445
+ http_sse_write(data->info.arg, .data = msg->msg);
446
+ return;
447
+ default:
448
+ fio_write(data->info.uuid, msg->msg.data, msg->msg.len);
449
+ return;
450
+ }
451
+ }
452
+ default:
453
+ if (data && data->info.uuid != -1) {
454
+ fio_protocol_s *pr =
455
+ fio_protocol_try_lock(data->info.uuid, FIO_PR_LOCK_TASK);
456
+ if (!pr) {
457
+ // perror("Connection lock failed");
458
+ if (errno != EBADF)
459
+ fio_message_defer(msg);
460
+ break;
461
+ }
462
+ IodineCaller.enterGVL(iodine_on_pubsub_call_block, msg);
463
+ fio_protocol_unlock(pr, FIO_PR_LOCK_TASK);
464
+ } else {
465
+ IodineCaller.enterGVL(iodine_on_pubsub_call_block, msg);
466
+ }
467
+ break;
468
+ }
469
+ }
470
+
471
+ /* callback for subscription closure */
472
+ static void iodine_on_unsubscribe(void *udata1, void *udata2) {
473
+ iodine_connection_data_s *data = udata1;
474
+ VALUE block = (VALUE)udata2;
475
+ switch (block) {
476
+ case Qnil:
477
+ if (data && data->info.type == IODINE_CONNECTION_WEBSOCKET) {
478
+ websocket_optimize4broadcasts(WEBSOCKET_OPTIMIZE_PUBSUB, 0);
479
+ }
480
+ break;
481
+ case Qtrue:
482
+ if (data && data->info.type == IODINE_CONNECTION_WEBSOCKET) {
483
+ websocket_optimize4broadcasts(WEBSOCKET_OPTIMIZE_PUBSUB_BINARY, 0);
484
+ }
485
+ break;
486
+ default:
487
+ IodineStore.remove(block);
488
+ break;
489
+ }
490
+ if (data) {
491
+ iodine_connection_data_free(data);
492
+ }
493
+ }
494
+
495
+ /* *****************************************************************************
496
+ Ruby Connection Methods - Pub/Sub
497
+ ***************************************************************************** */
498
+
499
+ typedef struct {
500
+ VALUE channel_org;
501
+ VALUE channel;
502
+ VALUE block;
503
+ fio_match_fn pattern;
504
+ uint8_t binary;
505
+ } iodine_sub_args_s;
506
+
507
+ /** Tests the `subscribe` Ruby arguments */
508
+ static iodine_sub_args_s iodine_subscribe_args(int argc, VALUE *argv) {
509
+
510
+ iodine_sub_args_s ret = {.channel_org = Qnil, .channel = Qnil, .block = Qnil};
511
+ VALUE rb_opt = 0;
512
+
513
+ switch (argc) {
514
+ case 2:
515
+ ret.channel = argv[0];
516
+ rb_opt = argv[1];
517
+ break;
518
+ case 1:
519
+ /* single argument must be a Hash / channel name */
520
+ if (TYPE(argv[0]) == T_HASH) {
521
+ rb_opt = argv[0];
522
+ ret.channel = rb_hash_aref(argv[0], ID2SYM(to_id));
523
+ if (ret.channel == Qnil || ret.channel == Qfalse) {
524
+ /* temporary backport support */
525
+ ret.channel = rb_hash_aref(argv[0], ID2SYM(channel_id));
526
+ if (ret.channel != Qnil) {
527
+ FIO_LOG_WARNING("use of :channel in subscribe is deprecated.");
528
+ }
529
+ }
530
+ } else {
531
+ ret.channel = argv[0];
532
+ }
533
+ break;
534
+ default:
535
+ rb_raise(rb_eArgError, "method accepts 1 or 2 arguments.");
536
+ return ret;
537
+ }
538
+ ret.channel_org = ret.channel;
539
+ if (ret.channel == Qnil || ret.channel == Qfalse) {
540
+ rb_raise(rb_eArgError,
541
+ "a target (:to) subject / stream / channel is required.");
542
+ return ret;
543
+ }
544
+
545
+ if (TYPE(ret.channel) == T_SYMBOL)
546
+ ret.channel = rb_sym2str(ret.channel);
547
+ Check_Type(ret.channel, T_STRING);
548
+
549
+ if (rb_opt) {
550
+ VALUE tmp = Qnil;
551
+ if ((tmp = rb_hash_aref(rb_opt, ID2SYM(as_id))) != Qnil &&
552
+ TYPE(tmp) == T_SYMBOL && rb_sym2id(tmp) == binary_id) {
553
+ ret.binary = 1;
554
+ }
555
+ if ((tmp = rb_hash_aref(rb_opt, ID2SYM(match_id))) != Qnil &&
556
+ TYPE(tmp) == T_SYMBOL && rb_sym2id(tmp) == redis_id) {
557
+ ret.pattern = FIO_MATCH_GLOB;
558
+ }
559
+ ret.block = rb_hash_aref(rb_opt, ID2SYM(handler_id));
560
+ if (ret.block != Qnil) {
561
+ IodineStore.add(ret.block);
562
+ }
563
+ }
564
+
565
+ if (ret.block == Qnil) {
566
+ if (rb_block_given_p()) {
567
+ ret.block = rb_block_proc();
568
+ IodineStore.add(ret.block);
569
+ }
570
+ }
571
+ return ret;
572
+ }
573
+
574
+ // clang-format off
575
+ /**
576
+ Subscribes to a Pub/Sub stream / channel or replaces an existing subscription.
577
+
578
+ The method accepts 1-2 arguments and an optional block. These are all valid ways
579
+ to call the method:
580
+
581
+ subscribe("my_stream") {|source, msg| p msg }
582
+ subscribe("my_strea*", match: :redis) {|source, msg| p msg }
583
+ subscribe(to: "my_stream") {|source, msg| p msg }
584
+ # or use any object that answers `#call(source, msg)`
585
+ MyProc = Proc.new {|source, msg| p msg }
586
+ subscribe to: "my_stream", match: :redis, handler: MyProc
587
+
588
+ The first argument must be either a String or a Hash.
589
+
590
+ The second, optional, argument must be a Hash (if given).
591
+
592
+ The options Hash supports the following possible keys (other keys are ignored, all keys are Symbols):
593
+
594
+ - `:match` - The channel / subject name matching type to be used. Valid value is: `:redis`. Future versions hope to support `:nats` and `:rabbit` patern matching as well.
595
+ - `:to` - The channel / subject to subscribe to.
596
+ - `:as` - (only for WebSocket connections) accepts the optional value `:binary`. default is `:text`. Note that binary transmissions are illegal for some connections (such as SSE) and an attempted binary subscription will fail for these connections.
597
+ - `:handler` - Any object that answers `.call(source, msg)` where source is the stream / channel name.
598
+
599
+ Note: if an existing subscription with the same name exists, it will be replaced by this new subscription.
600
+
601
+ Returns the name of the subscription, which matches the name be used in {unsubscribe} (or nil on failure).
602
+
603
+ */
604
+ static VALUE iodine_pubsub_subscribe(int argc, VALUE *argv, VALUE self) {
605
+ // clang-format on
606
+ iodine_sub_args_s args = iodine_subscribe_args(argc, argv);
607
+ if (args.channel == Qnil) {
608
+ return Qnil;
609
+ }
610
+ iodine_connection_data_s *c = NULL;
611
+ if (TYPE(self) == T_MODULE) {
612
+ if (!args.block) {
613
+ rb_raise(rb_eArgError,
614
+ "block or :handler required for local subscriptions.");
615
+ }
616
+ } else {
617
+ c = iodine_connection_validate_data(self);
618
+ if (!c || (c->info.type == IODINE_CONNECTION_SSE && args.binary)) {
619
+ if (args.block) {
620
+ IodineStore.remove(args.block);
621
+ }
622
+ return Qnil; /* cannot subscribe a closed / invalid connection. */
623
+ }
624
+ if (args.block == Qnil) {
625
+ if (c->info.type == IODINE_CONNECTION_WEBSOCKET)
626
+ websocket_optimize4broadcasts((args.binary
627
+ ? WEBSOCKET_OPTIMIZE_PUBSUB_BINARY
628
+ : WEBSOCKET_OPTIMIZE_PUBSUB),
629
+ 1);
630
+ if (args.binary) {
631
+ args.block = Qtrue;
632
+ }
633
+ }
634
+ fio_atomic_add(&c->ref, 1);
635
+ }
636
+
637
+ subscription_s *sub =
638
+ fio_subscribe(.channel = IODINE_RSTRINFO(args.channel),
639
+ .on_message = iodine_on_pubsub,
640
+ .on_unsubscribe = iodine_on_unsubscribe, .udata1 = c,
641
+ .udata2 = (void *)args.block, .match = args.pattern);
642
+ if (c) {
643
+ fio_lock(&c->lock);
644
+ if (c->info.uuid == -1) {
645
+ fio_unsubscribe(sub);
646
+ fio_unlock(&c->lock);
647
+ return Qnil;
648
+ }
649
+ iodine_sub_add(&c->subscriptions, sub);
650
+ fio_unlock(&c->lock);
651
+ } else {
652
+ fio_lock(&sub_lock);
653
+ iodine_sub_add(&sub_global, sub);
654
+ fio_unlock(&sub_lock);
655
+ }
656
+ return args.channel_org;
657
+ }
658
+
659
+ // clang-format off
660
+ /**
661
+ Unsubscribes from a Pub/Sub stream / channel.
662
+
663
+ The method accepts a single arguments, the name used for the subscription. i.e.:
664
+
665
+ subscribe("my_stream") {|source, msg| p msg }
666
+ unsubscribe("my_stream")
667
+
668
+ Returns `true` if the subscription was found.
669
+
670
+ Returns `false` if the subscription didn't exist.
671
+ */
672
+ static VALUE iodine_pubsub_unsubscribe(VALUE self, VALUE name) {
673
+ // clang-format on
674
+ iodine_connection_data_s *c = NULL;
675
+ fio_lock_i *s_lock = &sub_lock;
676
+ fio_subhash_s *subs = &sub_global;
677
+ VALUE ret;
678
+ if (TYPE(self) != T_MODULE) {
679
+ c = iodine_connection_validate_data(self);
680
+ if (!c || c->info.uuid == -1) {
681
+ return Qnil; /* cannot unsubscribe a closed connection. */
682
+ }
683
+ s_lock = &c->lock;
684
+ subs = &c->subscriptions;
685
+ }
686
+ if (TYPE(name) == T_SYMBOL)
687
+ name = rb_sym2str(name);
688
+ Check_Type(name, T_STRING);
689
+ fio_lock(s_lock);
690
+ ret = iodine_sub_unsubscribe(subs, IODINE_RSTRINFO(name));
691
+ fio_unlock(s_lock);
692
+ return ret;
693
+ }
694
+
695
+ // clang-format off
696
+ /**
697
+ Publishes a message to a channel.
698
+
699
+ Can be used using two Strings:
700
+
701
+ publish(to, message)
702
+
703
+ The method accepts an optional `engine` argument:
704
+
705
+ publish(to, message, my_pubsub_engine)
706
+
707
+ */
708
+ static VALUE iodine_pubsub_publish(int argc, VALUE *argv, VALUE self) {
709
+ // clang-format on
710
+ VALUE rb_ch, rb_msg, rb_engine = Qnil;
711
+ const fio_pubsub_engine_s *engine = NULL;
712
+ switch (argc) {
713
+ case 3:
714
+ /* fallthrough */
715
+ rb_engine = argv[2];
716
+ case 2:
717
+ rb_ch = argv[0];
718
+ rb_msg = argv[1];
719
+ break;
720
+ case 1: {
721
+ /* single argument must be a Hash */
722
+ Check_Type(argv[0], T_HASH);
723
+ rb_ch = rb_hash_aref(argv[0], to_id);
724
+ if (rb_ch == Qnil || rb_ch == Qfalse) {
725
+ rb_ch = rb_hash_aref(argv[0], channel_id);
726
+ }
727
+ rb_msg = rb_hash_aref(argv[0], message_id);
728
+ rb_engine = rb_hash_aref(argv[0], engine_id);
729
+ } break;
730
+ default:
731
+ rb_raise(rb_eArgError, "method accepts 1-3 arguments.");
732
+ }
733
+
734
+ if (rb_msg == Qnil || rb_msg == Qfalse) {
735
+ rb_raise(rb_eArgError, "message is required.");
736
+ }
737
+ Check_Type(rb_msg, T_STRING);
738
+
739
+ if (rb_ch == Qnil || rb_ch == Qfalse)
740
+ rb_raise(rb_eArgError, "target / channel is required .");
741
+ if (TYPE(rb_ch) == T_SYMBOL)
742
+ rb_ch = rb_sym2str(rb_ch);
743
+ Check_Type(rb_ch, T_STRING);
744
+
745
+ if (rb_engine == Qfalse) {
746
+ engine = FIO_PUBSUB_PROCESS;
747
+ } else if (rb_engine != Qnil) {
748
+ // collect engine object
749
+ iodine_pubsub_s *e = iodine_pubsub_CData(rb_engine);
750
+ if (e) {
751
+ engine = e->engine;
752
+ }
753
+ }
754
+
755
+ fio_publish(.engine = engine, .channel = IODINE_RSTRINFO(rb_ch),
756
+ .message = IODINE_RSTRINFO(rb_msg));
757
+ return Qtrue;
758
+ (void)self;
759
+ }
760
+
761
+ /* *****************************************************************************
762
+ Published C functions
763
+ ***************************************************************************** */
764
+
765
+ #undef iodine_connection_new
766
+ VALUE iodine_connection_new(iodine_connection_s args) {
767
+ VALUE connection = IodineCaller.call(ConnectionKlass, new_id);
768
+ if (connection == Qnil) {
769
+ return Qnil;
770
+ }
771
+ IodineStore.add(connection);
772
+ iodine_connection_data_s *data = iodine_connection_ruby2C(connection);
773
+ if (data == NULL) {
774
+ FIO_LOG_ERROR("(iodine) internal error, connection object has no C data!");
775
+ return Qnil;
776
+ }
777
+ *data = (iodine_connection_data_s){
778
+ .info = args,
779
+ .subscriptions = FIO_SET_INIT,
780
+ .ref = 1,
781
+ .answers_on_open = (rb_respond_to(args.handler, on_open_id) != 0),
782
+ .answers_on_message = (rb_respond_to(args.handler, on_message_id) != 0),
783
+ .answers_ping = (rb_respond_to(args.handler, ping_id) != 0),
784
+ .answers_on_drained = (rb_respond_to(args.handler, on_drained_id) != 0),
785
+ .answers_on_shutdown = (rb_respond_to(args.handler, on_shutdown_id) != 0),
786
+ .answers_on_close = (rb_respond_to(args.handler, on_close_id) != 0),
787
+ .lock = FIO_LOCK_INIT,
788
+ };
789
+ return connection;
790
+ }
791
+
792
+ /** Fires a connection object's event */
793
+ void iodine_connection_fire_event(VALUE connection,
794
+ iodine_connection_event_type_e ev,
795
+ VALUE msg) {
796
+ if (!connection || connection == Qnil) {
797
+ FIO_LOG_ERROR(
798
+ "(iodine) nil connection handle used by an internal API call");
799
+ return;
800
+ }
801
+ iodine_connection_data_s *data = iodine_connection_validate_data(connection);
802
+ if (!data) {
803
+ FIO_LOG_ERROR("(iodine) invalid connection handle used by an "
804
+ "internal API call: %p",
805
+ (void *)connection);
806
+ return;
807
+ }
808
+ if (!data->info.handler || data->info.handler == Qnil) {
809
+ FIO_LOG_DEBUG("(iodine) invalid connection handler, can't fire event %d",
810
+ (int)ev);
811
+ return;
812
+ }
813
+ VALUE args[2] = {connection, msg};
814
+ switch (ev) {
815
+ case IODINE_CONNECTION_ON_OPEN:
816
+ if (data->answers_on_open) {
817
+ IodineCaller.call2(data->info.handler, on_open_id, 1, args);
818
+ }
819
+ break;
820
+ case IODINE_CONNECTION_ON_MESSAGE:
821
+ if (data->answers_on_message) {
822
+ IodineCaller.call2(data->info.handler, on_message_id, 2, args);
823
+ }
824
+ break;
825
+ case IODINE_CONNECTION_ON_DRAINED:
826
+ if (data->answers_on_drained) {
827
+ IodineCaller.call2(data->info.handler, on_drained_id, 1, args);
828
+ }
829
+ break;
830
+ case IODINE_CONNECTION_ON_SHUTDOWN:
831
+ if (data->answers_on_shutdown) {
832
+ IodineCaller.call2(data->info.handler, on_shutdown_id, 1, args);
833
+ }
834
+ break;
835
+ case IODINE_CONNECTION_PING:
836
+ if (data->answers_ping) {
837
+ IodineCaller.call2(data->info.handler, ping_id, 1, args);
838
+ }
839
+ break;
840
+
841
+ case IODINE_CONNECTION_ON_CLOSE:
842
+ if (data->answers_on_close) {
843
+ IodineCaller.call2(data->info.handler, on_close_id, 1, args);
844
+ }
845
+ fio_lock(&data->lock);
846
+ iodine_sub_clear_all(&data->subscriptions);
847
+ data->info.handler = Qnil;
848
+ data->info.env = Qnil;
849
+ data->info.uuid = -1;
850
+ data->info.arg = NULL;
851
+ fio_unlock(&data->lock);
852
+ IodineStore.remove(connection);
853
+ break;
854
+ default:
855
+ break;
856
+ }
857
+ }
858
+
859
+ void iodine_connection_init(void) {
860
+ // set used constants
861
+ IodineUTF8Encoding = rb_enc_find("UTF-8");
862
+ // used ID objects
863
+ new_id = rb_intern2("new", 3);
864
+ call_id = rb_intern2("call", 4);
865
+
866
+ to_id = rb_intern2("to", 2);
867
+ channel_id = rb_intern2("channel", 7);
868
+ as_id = rb_intern2("as", 2);
869
+ binary_id = rb_intern2("binary", 6);
870
+ match_id = rb_intern2("match", 5);
871
+ redis_id = rb_intern2("redis", 5);
872
+ handler_id = rb_intern2("handler", 7);
873
+ engine_id = rb_intern2("engine", 6);
874
+ message_id = rb_intern2("message", 7);
875
+ on_open_id = rb_intern("on_open");
876
+ on_message_id = rb_intern("on_message");
877
+ on_drained_id = rb_intern("on_drained");
878
+ on_shutdown_id = rb_intern("on_shutdown");
879
+ on_close_id = rb_intern("on_close");
880
+ ping_id = rb_intern("ping");
881
+
882
+ // globalize ID objects
883
+ if (1) {
884
+ IodineStore.add(ID2SYM(to_id));
885
+ IodineStore.add(ID2SYM(channel_id));
886
+ IodineStore.add(ID2SYM(as_id));
887
+ IodineStore.add(ID2SYM(binary_id));
888
+ IodineStore.add(ID2SYM(match_id));
889
+ IodineStore.add(ID2SYM(redis_id));
890
+ IodineStore.add(ID2SYM(handler_id));
891
+ IodineStore.add(ID2SYM(engine_id));
892
+ IodineStore.add(ID2SYM(message_id));
893
+ IodineStore.add(ID2SYM(on_open_id));
894
+ IodineStore.add(ID2SYM(on_message_id));
895
+ IodineStore.add(ID2SYM(on_drained_id));
896
+ IodineStore.add(ID2SYM(on_shutdown_id));
897
+ IodineStore.add(ID2SYM(on_close_id));
898
+ IodineStore.add(ID2SYM(ping_id));
899
+ }
900
+
901
+ // should these be globalized?
902
+ WebSocketSymbol = ID2SYM(rb_intern("websocket"));
903
+ SSESymbol = ID2SYM(rb_intern("sse"));
904
+ RAWSymbol = ID2SYM(rb_intern("raw"));
905
+ IodineStore.add(WebSocketSymbol);
906
+ IodineStore.add(SSESymbol);
907
+ IodineStore.add(RAWSymbol);
908
+
909
+ // define the Connection Class and it's methods
910
+ ConnectionKlass =
911
+ rb_define_class_under(IodineModule, "Connection", rb_cObject);
912
+ rb_define_alloc_func(ConnectionKlass, iodine_connection_data_alloc_c);
913
+ rb_define_method(ConnectionKlass, "write", iodine_connection_write, 1);
914
+ rb_define_method(ConnectionKlass, "close", iodine_connection_close, 0);
915
+ rb_define_method(ConnectionKlass, "open?", iodine_connection_is_open, 0);
916
+ rb_define_method(ConnectionKlass, "pending", iodine_connection_pending, 0);
917
+ rb_define_method(ConnectionKlass, "protocol", iodine_connection_protocol_name,
918
+ 0);
919
+ rb_define_method(ConnectionKlass, "timeout", iodine_connection_timeout_get,
920
+ 0);
921
+ rb_define_method(ConnectionKlass, "timeout=", iodine_connection_timeout_set,
922
+ 1);
923
+ rb_define_method(ConnectionKlass, "env", iodine_connection_env, 0);
924
+
925
+ rb_define_method(ConnectionKlass, "handler", iodine_connection_handler_get,
926
+ 0);
927
+ rb_define_method(ConnectionKlass, "handler=", iodine_connection_handler_set,
928
+ 1);
929
+ rb_define_method(ConnectionKlass, "pubsub?", iodine_connection_is_pubsub, 0);
930
+ rb_define_method(ConnectionKlass, "subscribe", iodine_pubsub_subscribe, -1);
931
+ rb_define_method(ConnectionKlass, "unsubscribe", iodine_pubsub_unsubscribe,
932
+ 1);
933
+ rb_define_method(ConnectionKlass, "publish", iodine_pubsub_publish, -1);
934
+
935
+ // define global methods
936
+ rb_define_module_function(IodineModule, "subscribe", iodine_pubsub_subscribe,
937
+ -1);
938
+ rb_define_module_function(IodineModule, "unsubscribe",
939
+ iodine_pubsub_unsubscribe, 1);
940
+ rb_define_module_function(IodineModule, "publish", iodine_pubsub_publish, -1);
941
+ }