isomorfeus-iodine 0.7.44

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
  3. data/.gitignore +20 -0
  4. data/.rspec +2 -0
  5. data/.travis.yml +32 -0
  6. data/.yardopts +8 -0
  7. data/CHANGELOG.md +1038 -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 +44 -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/config.ru +56 -0
  25. data/examples/echo.ru +59 -0
  26. data/examples/hello.ru +29 -0
  27. data/examples/pubsub_engine.ru +81 -0
  28. data/examples/redis.ru +70 -0
  29. data/examples/shootout.ru +73 -0
  30. data/examples/sub-protocols.ru +90 -0
  31. data/examples/tcp_client.rb +66 -0
  32. data/examples/x-sendfile.ru +14 -0
  33. data/exe/iodine +277 -0
  34. data/ext/iodine/extconf.rb +109 -0
  35. data/ext/iodine/fio.c +11985 -0
  36. data/ext/iodine/fio.h +6373 -0
  37. data/ext/iodine/fio_cli.c +431 -0
  38. data/ext/iodine/fio_cli.h +189 -0
  39. data/ext/iodine/fio_json_parser.h +687 -0
  40. data/ext/iodine/fio_siphash.c +157 -0
  41. data/ext/iodine/fio_siphash.h +37 -0
  42. data/ext/iodine/fio_tls.h +129 -0
  43. data/ext/iodine/fio_tls_missing.c +649 -0
  44. data/ext/iodine/fio_tls_openssl.c +1056 -0
  45. data/ext/iodine/fio_tmpfile.h +50 -0
  46. data/ext/iodine/fiobj.h +44 -0
  47. data/ext/iodine/fiobj4fio.h +21 -0
  48. data/ext/iodine/fiobj_ary.c +333 -0
  49. data/ext/iodine/fiobj_ary.h +139 -0
  50. data/ext/iodine/fiobj_data.c +1185 -0
  51. data/ext/iodine/fiobj_data.h +167 -0
  52. data/ext/iodine/fiobj_hash.c +409 -0
  53. data/ext/iodine/fiobj_hash.h +176 -0
  54. data/ext/iodine/fiobj_json.c +622 -0
  55. data/ext/iodine/fiobj_json.h +68 -0
  56. data/ext/iodine/fiobj_mem.h +71 -0
  57. data/ext/iodine/fiobj_mustache.c +317 -0
  58. data/ext/iodine/fiobj_mustache.h +62 -0
  59. data/ext/iodine/fiobj_numbers.c +344 -0
  60. data/ext/iodine/fiobj_numbers.h +127 -0
  61. data/ext/iodine/fiobj_str.c +433 -0
  62. data/ext/iodine/fiobj_str.h +172 -0
  63. data/ext/iodine/fiobject.c +620 -0
  64. data/ext/iodine/fiobject.h +654 -0
  65. data/ext/iodine/hpack.h +1923 -0
  66. data/ext/iodine/http.c +2754 -0
  67. data/ext/iodine/http.h +1002 -0
  68. data/ext/iodine/http1.c +912 -0
  69. data/ext/iodine/http1.h +29 -0
  70. data/ext/iodine/http1_parser.h +873 -0
  71. data/ext/iodine/http_internal.c +1278 -0
  72. data/ext/iodine/http_internal.h +237 -0
  73. data/ext/iodine/http_mime_parser.h +350 -0
  74. data/ext/iodine/iodine.c +1430 -0
  75. data/ext/iodine/iodine.h +63 -0
  76. data/ext/iodine/iodine_caller.c +218 -0
  77. data/ext/iodine/iodine_caller.h +27 -0
  78. data/ext/iodine/iodine_connection.c +933 -0
  79. data/ext/iodine/iodine_connection.h +55 -0
  80. data/ext/iodine/iodine_defer.c +420 -0
  81. data/ext/iodine/iodine_defer.h +6 -0
  82. data/ext/iodine/iodine_fiobj2rb.h +120 -0
  83. data/ext/iodine/iodine_helpers.c +282 -0
  84. data/ext/iodine/iodine_helpers.h +12 -0
  85. data/ext/iodine/iodine_http.c +1171 -0
  86. data/ext/iodine/iodine_http.h +23 -0
  87. data/ext/iodine/iodine_json.c +302 -0
  88. data/ext/iodine/iodine_json.h +6 -0
  89. data/ext/iodine/iodine_mustache.c +567 -0
  90. data/ext/iodine/iodine_mustache.h +6 -0
  91. data/ext/iodine/iodine_pubsub.c +580 -0
  92. data/ext/iodine/iodine_pubsub.h +26 -0
  93. data/ext/iodine/iodine_rack_io.c +281 -0
  94. data/ext/iodine/iodine_rack_io.h +20 -0
  95. data/ext/iodine/iodine_store.c +142 -0
  96. data/ext/iodine/iodine_store.h +20 -0
  97. data/ext/iodine/iodine_tcp.c +346 -0
  98. data/ext/iodine/iodine_tcp.h +13 -0
  99. data/ext/iodine/iodine_tls.c +261 -0
  100. data/ext/iodine/iodine_tls.h +13 -0
  101. data/ext/iodine/mustache_parser.h +1546 -0
  102. data/ext/iodine/redis_engine.c +957 -0
  103. data/ext/iodine/redis_engine.h +79 -0
  104. data/ext/iodine/resp_parser.h +317 -0
  105. data/ext/iodine/websocket_parser.h +505 -0
  106. data/ext/iodine/websockets.c +735 -0
  107. data/ext/iodine/websockets.h +185 -0
  108. data/isomorfeus-iodine.gemspec +42 -0
  109. data/lib/iodine/connection.rb +61 -0
  110. data/lib/iodine/json.rb +42 -0
  111. data/lib/iodine/mustache.rb +113 -0
  112. data/lib/iodine/pubsub.rb +55 -0
  113. data/lib/iodine/rack_utils.rb +43 -0
  114. data/lib/iodine/tls.rb +16 -0
  115. data/lib/iodine/version.rb +3 -0
  116. data/lib/iodine.rb +274 -0
  117. data/lib/rack/handler/iodine.rb +33 -0
  118. data/logo.png +0 -0
  119. metadata +271 -0
@@ -0,0 +1,933 @@
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;
501
+ VALUE block;
502
+ fio_match_fn pattern;
503
+ uint8_t binary;
504
+ } iodine_sub_args_s;
505
+
506
+ /** Tests the `subscribe` Ruby arguments */
507
+ static iodine_sub_args_s iodine_subscribe_args(int argc, VALUE *argv) {
508
+
509
+ iodine_sub_args_s ret = {.channel = Qnil, .block = Qnil};
510
+ VALUE rb_opt = 0;
511
+
512
+ switch (argc) {
513
+ case 2:
514
+ ret.channel = argv[0];
515
+ rb_opt = argv[1];
516
+ break;
517
+ case 1:
518
+ /* single argument must be a Hash / channel name */
519
+ if (TYPE(argv[0]) == T_HASH) {
520
+ rb_opt = argv[0];
521
+ ret.channel = rb_hash_aref(argv[0], to_id);
522
+ if (ret.channel == Qnil || ret.channel == Qfalse) {
523
+ /* temporary backport support */
524
+ ret.channel = rb_hash_aref(argv[0], channel_id);
525
+ if (ret.channel != Qnil) {
526
+ FIO_LOG_WARNING("use of :channel in subscribe is deprecated.");
527
+ }
528
+ }
529
+ } else {
530
+ ret.channel = argv[0];
531
+ }
532
+ break;
533
+ default:
534
+ rb_raise(rb_eArgError, "method accepts 1 or 2 arguments.");
535
+ return ret;
536
+ }
537
+
538
+ if (ret.channel == Qnil || ret.channel == Qfalse) {
539
+ rb_raise(rb_eArgError,
540
+ "a target (:to) subject / stream / channel is required.");
541
+ return ret;
542
+ }
543
+
544
+ if (TYPE(ret.channel) == T_SYMBOL)
545
+ ret.channel = rb_sym2str(ret.channel);
546
+ Check_Type(ret.channel, T_STRING);
547
+
548
+ if (rb_opt) {
549
+ if (rb_hash_aref(rb_opt, as_id) == binary_id) {
550
+ ret.binary = 1;
551
+ }
552
+ if (rb_hash_aref(rb_opt, match_id) == redis_id) {
553
+ ret.pattern = FIO_MATCH_GLOB;
554
+ }
555
+ ret.block = rb_hash_aref(rb_opt, handler_id);
556
+ if (ret.block != Qnil) {
557
+ IodineStore.add(ret.block);
558
+ }
559
+ }
560
+
561
+ if (ret.block == Qnil) {
562
+ if (rb_block_given_p()) {
563
+ ret.block = rb_block_proc();
564
+ IodineStore.add(ret.block);
565
+ }
566
+ }
567
+ return ret;
568
+ }
569
+
570
+ // clang-format off
571
+ /**
572
+ Subscribes to a Pub/Sub stream / channel or replaces an existing subscription.
573
+
574
+ The method accepts 1-2 arguments and an optional block. These are all valid ways
575
+ to call the method:
576
+
577
+ subscribe("my_stream") {|source, msg| p msg }
578
+ subscribe("my_strea*", match: :redis) {|source, msg| p msg }
579
+ subscribe(to: "my_stream") {|source, msg| p msg }
580
+ # or use any object that answers `#call(source, msg)`
581
+ MyProc = Proc.new {|source, msg| p msg }
582
+ subscribe to: "my_stream", match: :redis, handler: MyProc
583
+
584
+ The first argument must be either a String or a Hash.
585
+
586
+ The second, optional, argument must be a Hash (if given).
587
+
588
+ The options Hash supports the following possible keys (other keys are ignored, all keys are Symbols):
589
+
590
+ - `: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.
591
+ - `:to` - The channel / subject to subscribe to.
592
+ - `: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.
593
+ - `:handler` - Any object that answers `.call(source, msg)` where source is the stream / channel name.
594
+
595
+ Note: if an existing subscription with the same name exists, it will be replaced by this new subscription.
596
+
597
+ Returns the name of the subscription, which matches the name be used in {unsubscribe} (or nil on failure).
598
+
599
+ */
600
+ static VALUE iodine_pubsub_subscribe(int argc, VALUE *argv, VALUE self) {
601
+ // clang-format on
602
+ iodine_sub_args_s args = iodine_subscribe_args(argc, argv);
603
+ if (args.channel == Qnil) {
604
+ return Qnil;
605
+ }
606
+ iodine_connection_data_s *c = NULL;
607
+ if (TYPE(self) == T_MODULE) {
608
+ if (!args.block) {
609
+ rb_raise(rb_eArgError,
610
+ "block or :handler required for local subscriptions.");
611
+ }
612
+ } else {
613
+ c = iodine_connection_validate_data(self);
614
+ if (!c || (c->info.type == IODINE_CONNECTION_SSE && args.binary)) {
615
+ if (args.block) {
616
+ IodineStore.remove(args.block);
617
+ }
618
+ return Qnil; /* cannot subscribe a closed / invalid connection. */
619
+ }
620
+ if (args.block == Qnil) {
621
+ if (c->info.type == IODINE_CONNECTION_WEBSOCKET)
622
+ websocket_optimize4broadcasts((args.binary
623
+ ? WEBSOCKET_OPTIMIZE_PUBSUB_BINARY
624
+ : WEBSOCKET_OPTIMIZE_PUBSUB),
625
+ 1);
626
+ if (args.binary) {
627
+ args.block = Qtrue;
628
+ }
629
+ }
630
+ fio_atomic_add(&c->ref, 1);
631
+ }
632
+
633
+ subscription_s *sub =
634
+ fio_subscribe(.channel = IODINE_RSTRINFO(args.channel),
635
+ .on_message = iodine_on_pubsub,
636
+ .on_unsubscribe = iodine_on_unsubscribe, .udata1 = c,
637
+ .udata2 = (void *)args.block, .match = args.pattern);
638
+ if (c) {
639
+ fio_lock(&c->lock);
640
+ if (c->info.uuid == -1) {
641
+ fio_unsubscribe(sub);
642
+ fio_unlock(&c->lock);
643
+ return Qnil;
644
+ }
645
+ iodine_sub_add(&c->subscriptions, sub);
646
+ fio_unlock(&c->lock);
647
+ } else {
648
+ fio_lock(&sub_lock);
649
+ iodine_sub_add(&sub_global, sub);
650
+ fio_unlock(&sub_lock);
651
+ }
652
+ return args.channel;
653
+ }
654
+
655
+ // clang-format off
656
+ /**
657
+ Unsubscribes from a Pub/Sub stream / channel.
658
+
659
+ The method accepts a single arguments, the name used for the subscription. i.e.:
660
+
661
+ subscribe("my_stream") {|source, msg| p msg }
662
+ unsubscribe("my_stream")
663
+
664
+ Returns `true` if the subscription was found.
665
+
666
+ Returns `false` if the subscription didn't exist.
667
+ */
668
+ static VALUE iodine_pubsub_unsubscribe(VALUE self, VALUE name) {
669
+ // clang-format on
670
+ iodine_connection_data_s *c = NULL;
671
+ fio_lock_i *s_lock = &sub_lock;
672
+ fio_subhash_s *subs = &sub_global;
673
+ VALUE ret;
674
+ if (TYPE(self) != T_MODULE) {
675
+ c = iodine_connection_validate_data(self);
676
+ if (!c || c->info.uuid == -1) {
677
+ return Qnil; /* cannot unsubscribe a closed connection. */
678
+ }
679
+ s_lock = &c->lock;
680
+ subs = &c->subscriptions;
681
+ }
682
+ fio_lock(s_lock);
683
+ ret = iodine_sub_unsubscribe(subs, IODINE_RSTRINFO(name));
684
+ fio_unlock(s_lock);
685
+ return ret;
686
+ }
687
+
688
+ // clang-format off
689
+ /**
690
+ Publishes a message to a channel.
691
+
692
+ Can be used using two Strings:
693
+
694
+ publish(to, message)
695
+
696
+ The method accepts an optional `engine` argument:
697
+
698
+ publish(to, message, my_pubsub_engine)
699
+
700
+ */
701
+ static VALUE iodine_pubsub_publish(int argc, VALUE *argv, VALUE self) {
702
+ // clang-format on
703
+ VALUE rb_ch, rb_msg, rb_engine = Qnil;
704
+ const fio_pubsub_engine_s *engine = NULL;
705
+ switch (argc) {
706
+ case 3:
707
+ /* fallthrough */
708
+ rb_engine = argv[2];
709
+ case 2:
710
+ rb_ch = argv[0];
711
+ rb_msg = argv[1];
712
+ break;
713
+ case 1: {
714
+ /* single argument must be a Hash */
715
+ Check_Type(argv[0], T_HASH);
716
+ rb_ch = rb_hash_aref(argv[0], to_id);
717
+ if (rb_ch == Qnil || rb_ch == Qfalse) {
718
+ rb_ch = rb_hash_aref(argv[0], channel_id);
719
+ }
720
+ rb_msg = rb_hash_aref(argv[0], message_id);
721
+ rb_engine = rb_hash_aref(argv[0], engine_id);
722
+ } break;
723
+ default:
724
+ rb_raise(rb_eArgError, "method accepts 1-3 arguments.");
725
+ }
726
+
727
+ if (rb_msg == Qnil || rb_msg == Qfalse) {
728
+ rb_raise(rb_eArgError, "message is required.");
729
+ }
730
+ Check_Type(rb_msg, T_STRING);
731
+
732
+ if (rb_ch == Qnil || rb_ch == Qfalse)
733
+ rb_raise(rb_eArgError, "target / channel is required .");
734
+ if (TYPE(rb_ch) == T_SYMBOL)
735
+ rb_ch = rb_sym2str(rb_ch);
736
+ Check_Type(rb_ch, T_STRING);
737
+
738
+ if (rb_engine == Qfalse) {
739
+ engine = FIO_PUBSUB_PROCESS;
740
+ } else if (rb_engine != Qnil) {
741
+ // collect engine object
742
+ iodine_pubsub_s *e = iodine_pubsub_CData(rb_engine);
743
+ if (e) {
744
+ engine = e->engine;
745
+ }
746
+ }
747
+
748
+ fio_publish(.engine = engine, .channel = IODINE_RSTRINFO(rb_ch),
749
+ .message = IODINE_RSTRINFO(rb_msg));
750
+ return Qtrue;
751
+ (void)self;
752
+ }
753
+
754
+ /* *****************************************************************************
755
+ Published C functions
756
+ ***************************************************************************** */
757
+
758
+ #undef iodine_connection_new
759
+ VALUE iodine_connection_new(iodine_connection_s args) {
760
+ VALUE connection = IodineCaller.call(ConnectionKlass, new_id);
761
+ if (connection == Qnil) {
762
+ return Qnil;
763
+ }
764
+ IodineStore.add(connection);
765
+ iodine_connection_data_s *data = iodine_connection_ruby2C(connection);
766
+ if (data == NULL) {
767
+ FIO_LOG_ERROR("(iodine) internal error, connection object has no C data!");
768
+ return Qnil;
769
+ }
770
+ *data = (iodine_connection_data_s){
771
+ .info = args,
772
+ .subscriptions = FIO_SET_INIT,
773
+ .ref = 1,
774
+ .answers_on_open = (rb_respond_to(args.handler, on_open_id) != 0),
775
+ .answers_on_message = (rb_respond_to(args.handler, on_message_id) != 0),
776
+ .answers_ping = (rb_respond_to(args.handler, ping_id) != 0),
777
+ .answers_on_drained = (rb_respond_to(args.handler, on_drained_id) != 0),
778
+ .answers_on_shutdown = (rb_respond_to(args.handler, on_shutdown_id) != 0),
779
+ .answers_on_close = (rb_respond_to(args.handler, on_close_id) != 0),
780
+ .lock = FIO_LOCK_INIT,
781
+ };
782
+ return connection;
783
+ }
784
+
785
+ /** Fires a connection object's event */
786
+ void iodine_connection_fire_event(VALUE connection,
787
+ iodine_connection_event_type_e ev,
788
+ VALUE msg) {
789
+ if (!connection || connection == Qnil) {
790
+ FIO_LOG_ERROR(
791
+ "(iodine) nil connection handle used by an internal API call");
792
+ return;
793
+ }
794
+ iodine_connection_data_s *data = iodine_connection_validate_data(connection);
795
+ if (!data) {
796
+ FIO_LOG_ERROR("(iodine) invalid connection handle used by an "
797
+ "internal API call: %p",
798
+ (void *)connection);
799
+ return;
800
+ }
801
+ if (!data->info.handler || data->info.handler == Qnil) {
802
+ FIO_LOG_DEBUG("(iodine) invalid connection handler, can't fire event %d",
803
+ (int)ev);
804
+ return;
805
+ }
806
+ VALUE args[2] = {connection, msg};
807
+ switch (ev) {
808
+ case IODINE_CONNECTION_ON_OPEN:
809
+ if (data->answers_on_open) {
810
+ IodineCaller.call2(data->info.handler, on_open_id, 1, args);
811
+ }
812
+ break;
813
+ case IODINE_CONNECTION_ON_MESSAGE:
814
+ if (data->answers_on_message) {
815
+ IodineCaller.call2(data->info.handler, on_message_id, 2, args);
816
+ }
817
+ break;
818
+ case IODINE_CONNECTION_ON_DRAINED:
819
+ if (data->answers_on_drained) {
820
+ IodineCaller.call2(data->info.handler, on_drained_id, 1, args);
821
+ }
822
+ break;
823
+ case IODINE_CONNECTION_ON_SHUTDOWN:
824
+ if (data->answers_on_shutdown) {
825
+ IodineCaller.call2(data->info.handler, on_shutdown_id, 1, args);
826
+ }
827
+ break;
828
+ case IODINE_CONNECTION_PING:
829
+ if (data->answers_ping) {
830
+ IodineCaller.call2(data->info.handler, ping_id, 1, args);
831
+ }
832
+ break;
833
+
834
+ case IODINE_CONNECTION_ON_CLOSE:
835
+ if (data->answers_on_close) {
836
+ IodineCaller.call2(data->info.handler, on_close_id, 1, args);
837
+ }
838
+ fio_lock(&data->lock);
839
+ iodine_sub_clear_all(&data->subscriptions);
840
+ data->info.handler = Qnil;
841
+ data->info.env = Qnil;
842
+ data->info.uuid = -1;
843
+ data->info.arg = NULL;
844
+ fio_unlock(&data->lock);
845
+ IodineStore.remove(connection);
846
+ break;
847
+ default:
848
+ break;
849
+ }
850
+ }
851
+
852
+ void iodine_connection_init(void) {
853
+ // set used constants
854
+ IodineUTF8Encoding = rb_enc_find("UTF-8");
855
+ // used ID objects
856
+ new_id = rb_intern2("new", 3);
857
+ call_id = rb_intern2("call", 4);
858
+
859
+ to_id = rb_intern2("to", 2);
860
+ channel_id = rb_intern2("channel", 7);
861
+ as_id = rb_intern2("as", 2);
862
+ binary_id = rb_intern2("binary", 6);
863
+ match_id = rb_intern2("match", 5);
864
+ redis_id = rb_intern2("redis", 5);
865
+ handler_id = rb_intern2("handler", 7);
866
+ engine_id = rb_intern2("engine", 6);
867
+ message_id = rb_intern2("message", 7);
868
+ on_open_id = rb_intern("on_open");
869
+ on_message_id = rb_intern("on_message");
870
+ on_drained_id = rb_intern("on_drained");
871
+ on_shutdown_id = rb_intern("on_shutdown");
872
+ on_close_id = rb_intern("on_close");
873
+ ping_id = rb_intern("ping");
874
+
875
+ // globalize ID objects
876
+ if (1) {
877
+ IodineStore.add(ID2SYM(to_id));
878
+ IodineStore.add(ID2SYM(channel_id));
879
+ IodineStore.add(ID2SYM(as_id));
880
+ IodineStore.add(ID2SYM(binary_id));
881
+ IodineStore.add(ID2SYM(match_id));
882
+ IodineStore.add(ID2SYM(redis_id));
883
+ IodineStore.add(ID2SYM(handler_id));
884
+ IodineStore.add(ID2SYM(engine_id));
885
+ IodineStore.add(ID2SYM(message_id));
886
+ IodineStore.add(ID2SYM(on_open_id));
887
+ IodineStore.add(ID2SYM(on_message_id));
888
+ IodineStore.add(ID2SYM(on_drained_id));
889
+ IodineStore.add(ID2SYM(on_shutdown_id));
890
+ IodineStore.add(ID2SYM(on_close_id));
891
+ IodineStore.add(ID2SYM(ping_id));
892
+ }
893
+
894
+ // should these be globalized?
895
+ WebSocketSymbol = ID2SYM(rb_intern("websocket"));
896
+ SSESymbol = ID2SYM(rb_intern("sse"));
897
+ RAWSymbol = ID2SYM(rb_intern("raw"));
898
+ IodineStore.add(WebSocketSymbol);
899
+ IodineStore.add(SSESymbol);
900
+ IodineStore.add(RAWSymbol);
901
+
902
+ // define the Connection Class and it's methods
903
+ ConnectionKlass = rb_define_class_under(IodineModule, "Connection", rb_cData);
904
+ rb_define_alloc_func(ConnectionKlass, iodine_connection_data_alloc_c);
905
+ rb_define_method(ConnectionKlass, "write", iodine_connection_write, 1);
906
+ rb_define_method(ConnectionKlass, "close", iodine_connection_close, 0);
907
+ rb_define_method(ConnectionKlass, "open?", iodine_connection_is_open, 0);
908
+ rb_define_method(ConnectionKlass, "pending", iodine_connection_pending, 0);
909
+ rb_define_method(ConnectionKlass, "protocol", iodine_connection_protocol_name,
910
+ 0);
911
+ rb_define_method(ConnectionKlass, "timeout", iodine_connection_timeout_get,
912
+ 0);
913
+ rb_define_method(ConnectionKlass, "timeout=", iodine_connection_timeout_set,
914
+ 1);
915
+ rb_define_method(ConnectionKlass, "env", iodine_connection_env, 0);
916
+
917
+ rb_define_method(ConnectionKlass, "handler", iodine_connection_handler_get,
918
+ 0);
919
+ rb_define_method(ConnectionKlass, "handler=", iodine_connection_handler_set,
920
+ 1);
921
+ rb_define_method(ConnectionKlass, "pubsub?", iodine_connection_is_pubsub, 0);
922
+ rb_define_method(ConnectionKlass, "subscribe", iodine_pubsub_subscribe, -1);
923
+ rb_define_method(ConnectionKlass, "unsubscribe", iodine_pubsub_unsubscribe,
924
+ 1);
925
+ rb_define_method(ConnectionKlass, "publish", iodine_pubsub_publish, -1);
926
+
927
+ // define global methods
928
+ rb_define_module_function(IodineModule, "subscribe", iodine_pubsub_subscribe,
929
+ -1);
930
+ rb_define_module_function(IodineModule, "unsubscribe",
931
+ iodine_pubsub_unsubscribe, 1);
932
+ rb_define_module_function(IodineModule, "publish", iodine_pubsub_publish, -1);
933
+ }