iodine 0.7.59 → 0.8.0

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.
Files changed (160) hide show
  1. checksums.yaml +4 -4
  2. data/.clang-format +15 -0
  3. data/.rspec +1 -0
  4. data/.yardopts +1 -3
  5. data/CHANGELOG.md +132 -12
  6. data/Gemfile +5 -7
  7. data/LICENSE.txt +1 -1
  8. data/README.md +147 -671
  9. data/Rakefile +9 -16
  10. data/examples/chatroom.nru +9 -0
  11. data/examples/client +74 -0
  12. data/examples/config-hello.nru +9 -0
  13. data/examples/config-hello.ru +11 -0
  14. data/examples/config-hijack.ru +13 -0
  15. data/examples/config-shootout.ru +43 -0
  16. data/examples/config.nru +120 -0
  17. data/examples/config.rb +37 -0
  18. data/examples/config.ru +42 -53
  19. data/examples/fake +24 -0
  20. data/examples/hello_neorack +137 -0
  21. data/examples/routes.nru +29 -0
  22. data/examples/routes.ru +42 -0
  23. data/exe/iodine +218 -258
  24. data/ext/iodine/extconf.rb +27 -100
  25. data/ext/iodine/fio-stl.h +128336 -0
  26. data/ext/iodine/fio-stl.md +26396 -0
  27. data/ext/iodine/iodine.c +213 -1393
  28. data/ext/iodine/iodine.h +250 -55
  29. data/ext/iodine/iodine_arg_helper.h +329 -0
  30. data/ext/iodine/iodine_caller.h +234 -25
  31. data/ext/iodine/iodine_cli.h +434 -0
  32. data/ext/iodine/iodine_compression.h +312 -0
  33. data/ext/iodine/iodine_connection.h +3484 -34
  34. data/ext/iodine/iodine_core.h +363 -0
  35. data/ext/iodine/iodine_crypto.h +1441 -0
  36. data/ext/iodine/iodine_defer.h +225 -4
  37. data/ext/iodine/iodine_json.h +738 -4
  38. data/ext/iodine/iodine_listener.h +263 -0
  39. data/ext/iodine/iodine_log.h +297 -0
  40. data/ext/iodine/iodine_minimap.h +543 -0
  41. data/ext/iodine/iodine_mustache.h +461 -4
  42. data/ext/iodine/iodine_pubsub_eng.h +729 -0
  43. data/ext/iodine/iodine_pubsub_history.h +547 -0
  44. data/ext/iodine/iodine_pubsub_msg.h +228 -0
  45. data/ext/iodine/iodine_redis.h +322 -0
  46. data/ext/iodine/iodine_store.h +418 -16
  47. data/ext/iodine/iodine_threads.h +279 -0
  48. data/ext/iodine/iodine_tls.h +515 -6
  49. data/ext/iodine/iodine_utils.h +923 -0
  50. data/iodine.gemspec +37 -42
  51. data/lib/iodine/benchmark.rb +1690 -0
  52. data/lib/iodine/documentation.rb +3085 -0
  53. data/lib/iodine/version.rb +17 -1
  54. data/lib/iodine.rb +54 -255
  55. data/logo.svg +36 -0
  56. data/mustache.md +55 -0
  57. data/sig/iodine.rbs +4 -0
  58. metadata +58 -184
  59. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
  60. data/.github/workflows/ruby.yml +0 -42
  61. data/.gitignore +0 -20
  62. data/LIMITS.md +0 -41
  63. data/SPEC-PubSub-Draft.md +0 -159
  64. data/SPEC-WebSocket-Draft.md +0 -239
  65. data/bin/console +0 -22
  66. data/bin/info.md +0 -353
  67. data/bin/mustache_bench.rb +0 -100
  68. data/bin/poc/Gemfile.lock +0 -23
  69. data/bin/poc/README.md +0 -37
  70. data/bin/poc/config.ru +0 -66
  71. data/bin/poc/gemfile +0 -1
  72. data/bin/poc/www/index.html +0 -57
  73. data/examples/async_task.ru +0 -92
  74. data/examples/bates/README.md +0 -3
  75. data/examples/bates/config.ru +0 -342
  76. data/examples/bates/david+bold.pdf +0 -0
  77. data/examples/bates/public/drop-pdf.png +0 -0
  78. data/examples/bates/public/index.html +0 -600
  79. data/examples/echo.ru +0 -59
  80. data/examples/etag.ru +0 -16
  81. data/examples/hello.ru +0 -29
  82. data/examples/pubsub_engine.ru +0 -81
  83. data/examples/rack3.ru +0 -12
  84. data/examples/redis.ru +0 -70
  85. data/examples/shootout.ru +0 -73
  86. data/examples/sub-protocols.ru +0 -90
  87. data/examples/tcp_client.rb +0 -66
  88. data/examples/x-sendfile.ru +0 -14
  89. data/ext/iodine/fio.c +0 -11995
  90. data/ext/iodine/fio.h +0 -6371
  91. data/ext/iodine/fio_cli.c +0 -431
  92. data/ext/iodine/fio_cli.h +0 -189
  93. data/ext/iodine/fio_json_parser.h +0 -688
  94. data/ext/iodine/fio_siphash.c +0 -157
  95. data/ext/iodine/fio_siphash.h +0 -37
  96. data/ext/iodine/fio_tls.h +0 -129
  97. data/ext/iodine/fio_tls_missing.c +0 -649
  98. data/ext/iodine/fio_tls_openssl.c +0 -1106
  99. data/ext/iodine/fio_tmpfile.h +0 -50
  100. data/ext/iodine/fiobj.h +0 -44
  101. data/ext/iodine/fiobj4fio.h +0 -21
  102. data/ext/iodine/fiobj_ary.c +0 -333
  103. data/ext/iodine/fiobj_ary.h +0 -139
  104. data/ext/iodine/fiobj_data.c +0 -1187
  105. data/ext/iodine/fiobj_data.h +0 -167
  106. data/ext/iodine/fiobj_hash.c +0 -409
  107. data/ext/iodine/fiobj_hash.h +0 -176
  108. data/ext/iodine/fiobj_json.c +0 -622
  109. data/ext/iodine/fiobj_json.h +0 -68
  110. data/ext/iodine/fiobj_mem.h +0 -71
  111. data/ext/iodine/fiobj_mustache.c +0 -317
  112. data/ext/iodine/fiobj_mustache.h +0 -62
  113. data/ext/iodine/fiobj_numbers.c +0 -344
  114. data/ext/iodine/fiobj_numbers.h +0 -127
  115. data/ext/iodine/fiobj_str.c +0 -433
  116. data/ext/iodine/fiobj_str.h +0 -172
  117. data/ext/iodine/fiobject.c +0 -620
  118. data/ext/iodine/fiobject.h +0 -655
  119. data/ext/iodine/hpack.h +0 -1923
  120. data/ext/iodine/http.c +0 -2743
  121. data/ext/iodine/http.h +0 -1002
  122. data/ext/iodine/http1.c +0 -910
  123. data/ext/iodine/http1.h +0 -29
  124. data/ext/iodine/http1_parser.h +0 -878
  125. data/ext/iodine/http_internal.c +0 -1279
  126. data/ext/iodine/http_internal.h +0 -237
  127. data/ext/iodine/http_mime_parser.h +0 -350
  128. data/ext/iodine/iodine_caller.c +0 -230
  129. data/ext/iodine/iodine_connection.c +0 -941
  130. data/ext/iodine/iodine_defer.c +0 -420
  131. data/ext/iodine/iodine_fiobj2rb.h +0 -120
  132. data/ext/iodine/iodine_helpers.c +0 -282
  133. data/ext/iodine/iodine_helpers.h +0 -12
  134. data/ext/iodine/iodine_http.c +0 -1181
  135. data/ext/iodine/iodine_http.h +0 -23
  136. data/ext/iodine/iodine_json.c +0 -302
  137. data/ext/iodine/iodine_mustache.c +0 -567
  138. data/ext/iodine/iodine_pubsub.c +0 -580
  139. data/ext/iodine/iodine_pubsub.h +0 -26
  140. data/ext/iodine/iodine_rack_io.c +0 -273
  141. data/ext/iodine/iodine_rack_io.h +0 -20
  142. data/ext/iodine/iodine_store.c +0 -146
  143. data/ext/iodine/iodine_tcp.c +0 -346
  144. data/ext/iodine/iodine_tcp.h +0 -13
  145. data/ext/iodine/iodine_tls.c +0 -261
  146. data/ext/iodine/mustache_parser.h +0 -1546
  147. data/ext/iodine/redis_engine.c +0 -957
  148. data/ext/iodine/redis_engine.h +0 -79
  149. data/ext/iodine/resp_parser.h +0 -317
  150. data/ext/iodine/websocket_parser.h +0 -506
  151. data/ext/iodine/websockets.c +0 -743
  152. data/ext/iodine/websockets.h +0 -185
  153. data/lib/iodine/connection.rb +0 -61
  154. data/lib/iodine/json.rb +0 -42
  155. data/lib/iodine/mustache.rb +0 -113
  156. data/lib/iodine/pubsub.rb +0 -55
  157. data/lib/iodine/rack_utils.rb +0 -43
  158. data/lib/iodine/tls.rb +0 -16
  159. data/lib/rack/handler/iodine.rb +0 -33
  160. data/logo.png +0 -0
@@ -0,0 +1,729 @@
1
+ #ifndef H___IODINE_PUBSUB_ENG___H
2
+ #define H___IODINE_PUBSUB_ENG___H
3
+ #include "iodine.h"
4
+
5
+ /* *****************************************************************************
6
+ Iodine PubSub Engine - Custom Publish/Subscribe Engine Support
7
+
8
+ This module provides the Iodine::PubSub::Engine Ruby class which allows
9
+ creating custom PubSub engines that can bridge Iodine's internal pub/sub
10
+ system with external message brokers (Redis, RabbitMQ, etc.).
11
+
12
+ A custom engine can implement any of these callbacks:
13
+ - subscribe(channel) - Called when subscribing to a channel
14
+ - psubscribe(pattern) - Called when subscribing to a pattern
15
+ - unsubscribe(channel) - Called when unsubscribing from a channel
16
+ - punsubscribe(pattern) - Called when unsubscribing from a pattern
17
+ - publish(message) - Called when publishing a message
18
+ - on_cleanup - Called when engine is detached
19
+
20
+ Built-in engines (constants on Iodine::PubSub):
21
+ - LOCAL - Publish within the local machine (master + all workers)
22
+ - CLUSTER - Publish to all workers across the cluster (default)
23
+
24
+ Ruby API:
25
+ - Iodine::PubSub.default = engine - Set default engine
26
+ - Iodine::PubSub.default - Get default engine
27
+ - Iodine::PubSub::Engine.new - Create custom engine (subclass this)
28
+ ***************************************************************************** */
29
+
30
+ /* *****************************************************************************
31
+ Ruby PubSub Engine Type
32
+ ***************************************************************************** */
33
+
34
+ /**
35
+ * Internal structure representing a PubSub engine.
36
+ *
37
+ * Wraps a facil.io pubsub engine with a Ruby handler object that
38
+ * receives callbacks for subscribe/unsubscribe/publish operations.
39
+ */
40
+ typedef struct iodine_pubsub_eng_s {
41
+ fio_pubsub_engine_s engine; /**< The facil.io engine callbacks */
42
+ fio_pubsub_engine_s *ptr; /**< Pointer to engine (self or built-in) */
43
+ VALUE handler; /**< Ruby handler object (self) */
44
+ } iodine_pubsub_eng_s;
45
+
46
+ /* *****************************************************************************
47
+ Ruby PubSub Engine Bridge - C to Ruby Callback Wrappers
48
+
49
+ These functions bridge the facil.io C callbacks to Ruby method calls.
50
+ They handle GVL acquisition and Ruby object creation/cleanup.
51
+ ***************************************************************************** */
52
+
53
+ /**
54
+ * Arguments passed to GVL-wrapped callback functions.
55
+ */
56
+ typedef struct iodine_pubsub_eng___args_s {
57
+ iodine_pubsub_eng_s *eng; /**< The engine receiving the callback */
58
+ const fio_pubsub_msg_s *msg; /**< Message for publish callbacks */
59
+ fio_buf_info_s channel; /**< Channel name for subscribe callbacks */
60
+ int16_t filter; /**< Filter value (reserved) */
61
+ } iodine_pubsub_eng___args_s;
62
+
63
+ /**
64
+ * Called after the engine was detached from the pubsub system.
65
+ * Invokes the Ruby handler's `on_cleanup` method for resource cleanup.
66
+ *
67
+ * @param eng The detached engine
68
+ */
69
+ static void iodine_pubsub_eng___detached(const fio_pubsub_engine_s *eng) {
70
+ iodine_pubsub_eng_s *e = (iodine_pubsub_eng_s *)eng;
71
+ iodine_ruby_call_anywhere(e->handler, rb_intern("on_cleanup"));
72
+ }
73
+
74
+ static void *iodine_pubsub_eng___subscribe__in_GC(void *a_) {
75
+ iodine_pubsub_eng___args_s *args = (iodine_pubsub_eng___args_s *)a_;
76
+ VALUE ch = rb_str_new(args->channel.buf, args->channel.len);
77
+ STORE.cache(ch);
78
+ iodine_ruby_call_inside(args->eng->handler, rb_intern("subscribe"), 1, &ch);
79
+ return NULL;
80
+ }
81
+ /**
82
+ * Subscribes to a channel. Called ONLY in the Root (master) process.
83
+ * Invokes the Ruby handler's `subscribe` method with the channel name.
84
+ *
85
+ * @param eng The pubsub engine
86
+ * @param channel The channel name to subscribe to
87
+ * @param filter Filter value (reserved for future use)
88
+ */
89
+ static void iodine_pubsub_eng___subscribe(const fio_pubsub_engine_s *eng,
90
+ fio_buf_info_s channel,
91
+ int16_t filter) {
92
+ iodine_pubsub_eng___args_s args = {
93
+ .eng = (iodine_pubsub_eng_s *)eng,
94
+ .channel = channel,
95
+ .filter = filter,
96
+ };
97
+ iodine_c_call_with(iodine_pubsub_eng___subscribe__in_GC, &args);
98
+ }
99
+
100
+ static void *iodine_pubsub_eng___psubscribe__in_GC(void *a_) {
101
+ iodine_pubsub_eng___args_s *args = (iodine_pubsub_eng___args_s *)a_;
102
+ VALUE ch = rb_str_new(args->channel.buf, args->channel.len);
103
+ STORE.cache(ch);
104
+ iodine_ruby_call_inside(args->eng->handler, rb_intern("psubscribe"), 1, &ch);
105
+ return NULL;
106
+ }
107
+ /**
108
+ * Subscribes to a pattern. Called ONLY in the Root (master) process.
109
+ * Invokes the Ruby handler's `psubscribe` method with the pattern.
110
+ *
111
+ * @param eng The pubsub engine
112
+ * @param channel The pattern to subscribe to
113
+ * @param filter Filter value (reserved for future use)
114
+ */
115
+ static void iodine_pubsub_eng___psubscribe(const fio_pubsub_engine_s *eng,
116
+ fio_buf_info_s channel,
117
+ int16_t filter) {
118
+ iodine_pubsub_eng___args_s args = {
119
+ .eng = (iodine_pubsub_eng_s *)eng,
120
+ .channel = channel,
121
+ .filter = filter,
122
+ };
123
+ iodine_c_call_with(iodine_pubsub_eng___psubscribe__in_GC, &args);
124
+ }
125
+
126
+ static void *iodine_pubsub_eng___unsubscribe__in_GC(void *a_) {
127
+ iodine_pubsub_eng___args_s *args = (iodine_pubsub_eng___args_s *)a_;
128
+ VALUE ch = rb_str_new(args->channel.buf, args->channel.len);
129
+ STORE.cache(ch);
130
+ iodine_ruby_call_inside(args->eng->handler, rb_intern("unsubscribe"), 1, &ch);
131
+ return NULL;
132
+ }
133
+ /**
134
+ * Unsubscribes from a channel. Called ONLY in the Root (master) process.
135
+ * Invokes the Ruby handler's `unsubscribe` method with the channel name.
136
+ *
137
+ * @param eng The pubsub engine
138
+ * @param channel The channel name to unsubscribe from
139
+ * @param filter Filter value (reserved for future use)
140
+ */
141
+ static void iodine_pubsub_eng___unsubscribe(const fio_pubsub_engine_s *eng,
142
+ fio_buf_info_s channel,
143
+ int16_t filter) {
144
+ iodine_pubsub_eng___args_s args = {
145
+ .eng = (iodine_pubsub_eng_s *)eng,
146
+ .channel = channel,
147
+ .filter = filter,
148
+ };
149
+ iodine_c_call_with(iodine_pubsub_eng___unsubscribe__in_GC, &args);
150
+ }
151
+
152
+ static void *iodine_pubsub_eng___punsubscribe__in_GC(void *a_) {
153
+ iodine_pubsub_eng___args_s *args = (iodine_pubsub_eng___args_s *)a_;
154
+ VALUE ch = rb_str_new(args->channel.buf, args->channel.len);
155
+ STORE.cache(ch);
156
+ iodine_ruby_call_inside(args->eng->handler,
157
+ rb_intern("punsubscribe"),
158
+ 1,
159
+ &ch);
160
+ return NULL;
161
+ }
162
+ /**
163
+ * Unsubscribes from a pattern. Called ONLY in the Root (master) process.
164
+ * Invokes the Ruby handler's `punsubscribe` method with the pattern.
165
+ *
166
+ * @param eng The pubsub engine
167
+ * @param channel The pattern to unsubscribe from
168
+ * @param filter Filter value (reserved for future use)
169
+ */
170
+ static void iodine_pubsub_eng___punsubscribe(const fio_pubsub_engine_s *eng,
171
+ fio_buf_info_s channel,
172
+ int16_t filter) {
173
+ iodine_pubsub_eng___args_s args = {
174
+ .eng = (iodine_pubsub_eng_s *)eng,
175
+ .channel = channel,
176
+ .filter = filter,
177
+ };
178
+ iodine_c_call_with(iodine_pubsub_eng___punsubscribe__in_GC, &args);
179
+ }
180
+
181
+ static void *iodine_pubsub_eng___publish__in_GC(void *a_) {
182
+ iodine_pubsub_eng___args_s *args = (iodine_pubsub_eng___args_s *)a_;
183
+ VALUE msg = iodine_pubsub_msg_new(args->msg);
184
+ STORE.cache(msg);
185
+ iodine_ruby_call_inside(args->eng->handler, rb_intern("publish"), 1, &msg);
186
+ return NULL;
187
+ }
188
+ /**
189
+ * Publishes a message through the engine. Called by any worker/thread.
190
+ * Invokes the Ruby handler's `publish` method with a Message object.
191
+ *
192
+ * publish is called INLINE by fio_pubsub_publish() — GVL state is ambiguous.
193
+ * When Ruby calls Iodine.publish with a custom engine, we already have GVL.
194
+ * When called from IO thread / IPC delivery, we don't.
195
+ *
196
+ * @param eng The pubsub engine
197
+ * @param msg The message to publish
198
+ */
199
+ static void iodine_pubsub_eng___publish(const fio_pubsub_engine_s *eng,
200
+ const fio_pubsub_msg_s *msg) {
201
+ iodine_pubsub_eng___args_s args = {
202
+ .eng = (iodine_pubsub_eng_s *)eng,
203
+ .msg = msg,
204
+ };
205
+ iodine_c_call_with(iodine_pubsub_eng___publish__in_GC, &args);
206
+ }
207
+
208
+ /**
209
+ * Validates a Ruby object and creates a pubsub engine struct.
210
+ *
211
+ * Checks which callback methods the Ruby object responds to and
212
+ * sets up the corresponding C callbacks. Methods not implemented
213
+ * by the Ruby object will have NULL callbacks.
214
+ *
215
+ * @param obj The Ruby engine object to validate
216
+ * @return A configured fio_pubsub_engine_s struct
217
+ */
218
+ static fio_pubsub_engine_s iodine_pubsub___engine_validate(VALUE obj) {
219
+ fio_pubsub_engine_s r = {
220
+ /* Called after the engine was detached, may be used for cleanup. */
221
+ .detached = (rb_respond_to(obj, rb_intern("on_cleanup")))
222
+ ? iodine_pubsub_eng___detached
223
+ : NULL,
224
+ /* Subscribes to a channel. Called ONLY in the Root (master) process. */
225
+ .subscribe = (rb_respond_to(obj, rb_intern("subscribe")))
226
+ ? iodine_pubsub_eng___subscribe
227
+ : NULL,
228
+ /* Subscribes to a pattern. Called ONLY in the Root (master) process. */
229
+ .psubscribe = (rb_respond_to(obj, rb_intern("psubscribe")))
230
+ ? iodine_pubsub_eng___psubscribe
231
+ : NULL,
232
+ /* Unsubscribes from a channel. Called ONLY in the Root (master) process.
233
+ */
234
+ .unsubscribe = (rb_respond_to(obj, rb_intern("unsubscribe")))
235
+ ? iodine_pubsub_eng___unsubscribe
236
+ : NULL,
237
+ /* Unsubscribes from a pattern. Called ONLY in the Root (master) process.
238
+ */
239
+ .punsubscribe = (rb_respond_to(obj, rb_intern("punsubscribe")))
240
+ ? iodine_pubsub_eng___punsubscribe
241
+ : NULL,
242
+ /* Publishes a message through the engine. Called by any worker/thread. */
243
+ .publish = (rb_respond_to(obj, rb_intern("publish")))
244
+ ? iodine_pubsub_eng___publish
245
+ : NULL,
246
+ };
247
+ return r;
248
+ }
249
+ /* *****************************************************************************
250
+ Ruby PubSub Engine Object - Ruby TypedData Wrapper
251
+ ***************************************************************************** */
252
+
253
+ static size_t iodine_pubsub_eng_data_size(const void *ptr_) {
254
+ iodine_pubsub_eng_s *m = (iodine_pubsub_eng_s *)ptr_;
255
+ return sizeof(*m);
256
+ }
257
+
258
+ static void iodine_pubsub_eng_free(void *ptr_) {
259
+ iodine_pubsub_eng_s *e = (iodine_pubsub_eng_s *)ptr_;
260
+ if (fio_pubsub_engine_default() == e->ptr)
261
+ fio_pubsub_engine_default_set(NULL);
262
+ ruby_xfree(e);
263
+ // FIO_LEAK_COUNTER_ON_FREE(iodine_pubsub_eng);
264
+ }
265
+
266
+ static const rb_data_type_t IODINE_PUBSUB_ENG_DATA_TYPE = {
267
+ .wrap_struct_name = "IodinePSEngine",
268
+ .function =
269
+ {
270
+ .dfree = iodine_pubsub_eng_free,
271
+ .dsize = iodine_pubsub_eng_data_size,
272
+ },
273
+ .data = NULL,
274
+ // .flags = RUBY_TYPED_FREE_IMMEDIATELY,
275
+ };
276
+
277
+ static VALUE iodine_pubsub_eng_alloc(VALUE klass) {
278
+ /* use Ruby allocator for long life object */
279
+ iodine_pubsub_eng_s *m = (iodine_pubsub_eng_s *)ruby_xmalloc(sizeof(*m));
280
+ if (!m)
281
+ goto no_memory;
282
+ *m = (iodine_pubsub_eng_s){0};
283
+ m->ptr = &m->engine;
284
+ // FIO_LEAK_COUNTER_ON_ALLOC(iodine_pubsub_eng);
285
+ m->handler = TypedData_Wrap_Struct(klass, &IODINE_PUBSUB_ENG_DATA_TYPE, m);
286
+ m->engine = iodine_pubsub___engine_validate(m->handler);
287
+ return m->handler;
288
+
289
+ no_memory:
290
+ FIO_LOG_FATAL("Memory allocation failed");
291
+ fio_io_stop();
292
+ return Qnil;
293
+ }
294
+
295
+ static iodine_pubsub_eng_s *iodine_pubsub_eng_get(VALUE self) {
296
+ iodine_pubsub_eng_s *m;
297
+ TypedData_Get_Struct(self,
298
+ iodine_pubsub_eng_s,
299
+ &IODINE_PUBSUB_ENG_DATA_TYPE,
300
+ m);
301
+ return m;
302
+ }
303
+
304
+ /* *****************************************************************************
305
+ Ruby Methods - Engine API
306
+ ***************************************************************************** */
307
+
308
+ /**
309
+ * Initializes a new PubSub engine and attaches it to the pubsub system.
310
+ *
311
+ * @param self The Engine instance
312
+ * @return self
313
+ *
314
+ * Ruby: engine = Iodine::PubSub::Engine.new
315
+ */
316
+ static VALUE iodine_pubsub_eng_initialize(VALUE self) {
317
+ iodine_pubsub_eng_s *m = iodine_pubsub_eng_get(self);
318
+ fio_pubsub_engine_attach(m->ptr);
319
+ return self;
320
+ }
321
+
322
+ /** Internal constant name for storing the default engine */
323
+ #define IODINE_PUBSUB_DEFAULT_NM "PUBSUB____DEFAULT"
324
+
325
+ /**
326
+ * Sets the default PubSub engine for all publish operations.
327
+ *
328
+ * @param klass The Iodine::PubSub module
329
+ * @param eng The engine to set as default (or nil for CLUSTER)
330
+ * @return The new default engine
331
+ *
332
+ * Ruby: Iodine::PubSub.default = my_engine
333
+ */
334
+ static VALUE iodine_pubsub_eng_default_set(VALUE klass, VALUE eng) {
335
+ fio_pubsub_engine_s *e = (fio_pubsub_engine_s *)fio_pubsub_engine_cluster();
336
+ ID name = rb_intern(IODINE_PUBSUB_DEFAULT_NM);
337
+ if (!IODINE_STORE_IS_SKIP(eng)) {
338
+ e = iodine_pubsub_eng_get(eng)->ptr;
339
+ }
340
+ fio_pubsub_engine_default_set(e);
341
+ VALUE old = rb_const_get(iodine_rb_IODINE_BASE, name);
342
+ if ((uintptr_t)old > 15)
343
+ STORE.release(old);
344
+ STORE.hold(eng);
345
+ rb_const_remove(iodine_rb_IODINE_BASE, name);
346
+ rb_const_set(iodine_rb_IODINE_BASE, name, eng);
347
+ return eng;
348
+ (void)klass;
349
+ }
350
+
351
+ /**
352
+ * Gets the current default PubSub engine.
353
+ *
354
+ * @param klass The Iodine::PubSub module
355
+ * @return The current default engine
356
+ *
357
+ * Ruby: Iodine::PubSub.default
358
+ */
359
+ static VALUE iodine_pubsub_eng_default_get(VALUE klass) {
360
+ return rb_const_get(iodine_rb_IODINE_BASE,
361
+ rb_intern(IODINE_PUBSUB_DEFAULT_NM));
362
+ (void)klass;
363
+ }
364
+
365
+ /* *****************************************************************************
366
+ Initialize - Ruby Class Registration
367
+ ***************************************************************************** */
368
+
369
+ /**
370
+ * Initializes the Iodine::PubSub::Engine Ruby class.
371
+ *
372
+ * Defines:
373
+ * - Iodine::PubSub.default / default= module methods
374
+ * - Iodine::PubSub::Engine class with initialize method
375
+ * - Built-in engine constants: LOCAL and CLUSTER
376
+ *
377
+ * Only two built-in engines are provided:
378
+ * - LOCAL (fio_pubsub_engine_ipc): Local machine only (master + all workers)
379
+ * - CLUSTER (fio_pubsub_engine_cluster): Multi-machine cluster (default)
380
+ */
381
+ static void Init_Iodine_PubSub_Engine(void) {
382
+ /** Initialize Iodine::PubSub::Engine */
383
+ rb_define_module_function(iodine_rb_IODINE_PUBSUB,
384
+ "default=",
385
+ iodine_pubsub_eng_default_set,
386
+ 1);
387
+ rb_define_module_function(iodine_rb_IODINE_PUBSUB,
388
+ "default",
389
+ iodine_pubsub_eng_default_get,
390
+ 0);
391
+
392
+ iodine_rb_IODINE_PUBSUB_ENG =
393
+ rb_define_class_under(iodine_rb_IODINE_PUBSUB, "Engine", rb_cObject);
394
+ STORE.hold(iodine_rb_IODINE_PUBSUB_ENG);
395
+ rb_define_alloc_func(iodine_rb_IODINE_PUBSUB_ENG, iodine_pubsub_eng_alloc);
396
+
397
+ /* Define LOCAL engine (IPC - local machine only) */
398
+ {
399
+ VALUE tmp = rb_obj_alloc(iodine_rb_IODINE_PUBSUB_ENG);
400
+ iodine_pubsub_eng_get(tmp)->ptr =
401
+ (fio_pubsub_engine_s *)fio_pubsub_engine_ipc();
402
+ rb_define_const(iodine_rb_IODINE_PUBSUB, "LOCAL", tmp);
403
+ }
404
+
405
+ /* Define CLUSTER engine (multi-machine cluster) */
406
+ {
407
+ VALUE tmp = rb_obj_alloc(iodine_rb_IODINE_PUBSUB_ENG);
408
+ iodine_pubsub_eng_get(tmp)->ptr =
409
+ (fio_pubsub_engine_s *)fio_pubsub_engine_cluster();
410
+ rb_define_const(iodine_rb_IODINE_PUBSUB, "CLUSTER", tmp);
411
+ }
412
+
413
+ rb_define_const(iodine_rb_IODINE_BASE,
414
+ IODINE_PUBSUB_DEFAULT_NM,
415
+ rb_const_get(iodine_rb_IODINE_PUBSUB, rb_intern("CLUSTER")));
416
+
417
+ rb_define_method(iodine_rb_IODINE_PUBSUB_ENG,
418
+ "initialize",
419
+ iodine_pubsub_eng_initialize,
420
+ 0);
421
+ }
422
+
423
+ /* *****************************************************************************
424
+ Iodine PubSub Subscription - Independent Non-IO Subscription Handle
425
+
426
+ This module provides the Iodine::PubSub::Subscription Ruby class which wraps
427
+ a non-IO-bound facil.io subscription. Unlike Iodine.subscribe (which allows
428
+ only one callback per channel per global context), each Subscription object
429
+ is its own independent context — multiple Subscription objects can subscribe
430
+ to the same channel simultaneously.
431
+
432
+ Features:
433
+ - Multiple independent subscriptions to the same channel
434
+ - Early cancellation via #cancel (idempotent)
435
+ - Live handler replacement via #handler=
436
+ - Global lifetime even without a caller-held reference
437
+ - Auto-cancel during FIO_CALL_AT_EXIT
438
+
439
+ Ruby API:
440
+ sub = Iodine::PubSub::Subscription.new("channel") { |msg| ... }
441
+ sub = Iodine::PubSub::Subscription.new(channel: "ch", filter: 0) { |msg| ... }
442
+ sub.handler # => current Proc (retained after cancellation)
443
+ sub.handler = proc # => proc (replaces callback for future messages)
444
+ sub.active? # => true/false
445
+ sub.cancel # => self (idempotent)
446
+ ***************************************************************************** */
447
+
448
+ /* *****************************************************************************
449
+ Ruby PubSub Subscription Type
450
+ ***************************************************************************** */
451
+
452
+ /**
453
+ * One native object is shared by the Ruby wrapper and facil.io.
454
+ *
455
+ * The wrapper is held in STORE for as long as the native subscription exists,
456
+ * so a subscription remains active even when the caller doesn't save it.
457
+ */
458
+ typedef struct iodine_pubsub_sub_s {
459
+ uintptr_t handle;
460
+ VALUE handler;
461
+ VALUE wrapper;
462
+ uint8_t initialized;
463
+ } iodine_pubsub_sub_s;
464
+
465
+ /** Requests cancellation. fio_pubsub_unsubscribe zeros the handle and is
466
+ * idempotent when guarded by the handle check. */
467
+ static void iodine_pubsub_sub_force_cleanup(void *sub_) {
468
+ iodine_pubsub_sub_s *s = (iodine_pubsub_sub_s *)sub_;
469
+ if (s->handle)
470
+ fio_pubsub_unsubscribe(.subscription_handle_ptr = &s->handle);
471
+ }
472
+
473
+ /* *****************************************************************************
474
+ Ruby PubSub Subscription - Message Callbacks
475
+ ***************************************************************************** */
476
+
477
+ /** Called inside the GVL to dispatch a pub/sub message. */
478
+ static void *iodine_pubsub_sub_on_message_in_gvl(void *m_) {
479
+ fio_pubsub_msg_s *m = (fio_pubsub_msg_s *)m_;
480
+ iodine_pubsub_sub_s *s = (iodine_pubsub_sub_s *)m->udata;
481
+ if (!s->handle || IODINE_STORE_IS_SKIP(s->handler))
482
+ return m_;
483
+ VALUE msg = iodine_pubsub_msg_new(m);
484
+ iodine_ruby_call_inside(s->handler, IODINE_CALL_ID, 1, &msg);
485
+ STORE.release(msg);
486
+ return m_;
487
+ }
488
+
489
+ /** Called by facil.io when a message arrives on the subscribed channel. */
490
+ static void iodine_pubsub_sub_on_message(fio_pubsub_msg_s *m) {
491
+ iodine_c_call_with(iodine_pubsub_sub_on_message_in_gvl, m);
492
+ }
493
+
494
+ /**
495
+ * Removes exit cleanup and releases the STORE-held Ruby wrapper after the
496
+ * native subscription is completely gone. Safe outside the GVL.
497
+ */
498
+ static void iodine_pubsub_sub_on_unsubscribe(void *udata) {
499
+ iodine_pubsub_sub_s *s = (iodine_pubsub_sub_s *)udata;
500
+ VALUE wrapper = s->wrapper;
501
+ s->handle = 0;
502
+ s->wrapper = Qnil;
503
+ fio_state_callback_remove(FIO_CALL_AT_EXIT,
504
+ iodine_pubsub_sub_force_cleanup,
505
+ s);
506
+ STORE.release(wrapper);
507
+ }
508
+
509
+ /* *****************************************************************************
510
+ Ruby PubSub Subscription Object - Ruby TypedData Wrapper
511
+ ***************************************************************************** */
512
+
513
+ static size_t iodine_pubsub_sub_data_size(const void *ptr_) {
514
+ (void)ptr_;
515
+ return sizeof(iodine_pubsub_sub_s);
516
+ }
517
+
518
+ /**
519
+ * GC mark callback — keeps the handler proc alive while the subscription is.
520
+ */
521
+ static void iodine_pubsub_sub_mark(void *ptr_) {
522
+ iodine_pubsub_sub_s *s = (iodine_pubsub_sub_s *)ptr_;
523
+ if (!IODINE_STORE_IS_SKIP(s->handler))
524
+ rb_gc_mark(s->handler);
525
+ }
526
+
527
+ /** The wrapper can only be collected after native unsubscribe releases it. */
528
+ static void iodine_pubsub_sub_free(void *ptr_) {
529
+ ruby_xfree(ptr_);
530
+ FIO_LEAK_COUNTER_ON_FREE(iodine_pubsub_sub);
531
+ }
532
+
533
+ static const rb_data_type_t IODINE_PUBSUB_SUB_DATA_TYPE = {
534
+ .wrap_struct_name = "IodinePSSub",
535
+ .function =
536
+ {
537
+ .dmark = iodine_pubsub_sub_mark,
538
+ .dfree = iodine_pubsub_sub_free,
539
+ .dsize = iodine_pubsub_sub_data_size,
540
+ },
541
+ .data = NULL,
542
+ // .flags = RUBY_TYPED_FREE_IMMEDIATELY,
543
+ };
544
+
545
+ static VALUE iodine_pubsub_sub_alloc(VALUE klass) {
546
+ iodine_pubsub_sub_s *s = (iodine_pubsub_sub_s *)ruby_xmalloc(sizeof(*s));
547
+ if (!s)
548
+ goto no_memory;
549
+ *s = (iodine_pubsub_sub_s){.handler = Qnil, .wrapper = Qnil};
550
+ FIO_LEAK_COUNTER_ON_ALLOC(iodine_pubsub_sub);
551
+ return TypedData_Wrap_Struct(klass, &IODINE_PUBSUB_SUB_DATA_TYPE, s);
552
+ no_memory:
553
+ FIO_LOG_FATAL("Memory allocation failed");
554
+ fio_io_stop();
555
+ return Qnil;
556
+ }
557
+
558
+ static iodine_pubsub_sub_s *iodine_pubsub_sub_get(VALUE self) {
559
+ iodine_pubsub_sub_s *s;
560
+ TypedData_Get_Struct(self,
561
+ iodine_pubsub_sub_s,
562
+ &IODINE_PUBSUB_SUB_DATA_TYPE,
563
+ s);
564
+ return s;
565
+ }
566
+
567
+ /* *****************************************************************************
568
+ Ruby Methods - Subscription API
569
+ ***************************************************************************** */
570
+
571
+ /**
572
+ * Initializes a new independent pub/sub subscription.
573
+ *
574
+ * Creates a non-IO-bound facil.io subscription using subscription_handle_ptr,
575
+ * allowing multiple independent subscriptions to the same channel.
576
+ *
577
+ * @param channel [String] the channel name to subscribe to
578
+ * @param filter [Integer] optional numerical filter (default: 0)
579
+ * @param since [Integer] optional replay-since timestamp in milliseconds
580
+ * @param block [Proc] required message handler callback
581
+ * @return self
582
+ *
583
+ * Ruby: sub = Iodine::PubSub::Subscription.new("channel") { |msg| ... }
584
+ * sub = Iodine::PubSub::Subscription.new(channel: "ch", filter: 0) {
585
+ * |msg| ... }
586
+ */
587
+ static VALUE iodine_pubsub_sub_initialize(int argc, VALUE *argv, VALUE self) {
588
+ iodine_pubsub_sub_s *s = iodine_pubsub_sub_get(self);
589
+ fio_buf_info_s channel = FIO_BUF_INFO2(NULL, 0);
590
+ int64_t filter = 0;
591
+ uint64_t since = 0;
592
+ VALUE proc = Qnil;
593
+
594
+ iodine_rb2c_arg(argc,
595
+ argv,
596
+ IODINE_ARG_BUF(channel, 0, "channel", 1),
597
+ IODINE_ARG_NUM(filter, 0, "filter", 0),
598
+ IODINE_ARG_U64(since, 0, "since", 0),
599
+ IODINE_ARG_PROC(proc, 0, "callback", 1));
600
+
601
+ if ((size_t)filter & (~(size_t)0xFFFF))
602
+ rb_raise(rb_eRangeError,
603
+ "filter out of range (%lld > 0xFFFF)",
604
+ (long long)filter);
605
+
606
+ if (s->initialized)
607
+ rb_raise(rb_eRuntimeError, "Subscription is already initialized");
608
+ s->initialized = 1;
609
+ s->handler = proc;
610
+ s->wrapper = self;
611
+ STORE.hold(self);
612
+ fio_state_callback_add(FIO_CALL_AT_EXIT,
613
+ iodine_pubsub_sub_force_cleanup,
614
+ s);
615
+
616
+ /* The same native object is both Ruby's TypedData and facil.io's udata. */
617
+ fio_pubsub_subscribe(.subscription_handle_ptr = &s->handle,
618
+ .filter = (int16_t)filter,
619
+ .channel = channel,
620
+ .udata = (void *)s,
621
+ .queue = fio_io_async_queue(&IODINE_THREAD_POOL),
622
+ .on_message = iodine_pubsub_sub_on_message,
623
+ .on_unsubscribe = iodine_pubsub_sub_on_unsubscribe,
624
+ .replay_since = since);
625
+ return self;
626
+ }
627
+
628
+ /**
629
+ * Returns the current message handler proc.
630
+ *
631
+ * @return [Proc, nil] the current handler, retained after cancellation
632
+ *
633
+ * Ruby: sub.handler # => Proc
634
+ */
635
+ static VALUE iodine_pubsub_sub_handler_get(VALUE self) {
636
+ iodine_pubsub_sub_s *s = iodine_pubsub_sub_get(self);
637
+ return s->handler;
638
+ }
639
+
640
+ /**
641
+ * Replaces the message handler proc.
642
+ *
643
+ * Updates the single handler field used by both #handler and message delivery.
644
+ * Both this method and message dispatch run under the GVL.
645
+ *
646
+ * @param new_handler [Proc] the new handler (must respond to #call)
647
+ * @return [Proc] the new handler
648
+ *
649
+ * Ruby: sub.handler = proc { |msg| ... }
650
+ */
651
+ static VALUE iodine_pubsub_sub_handler_set(VALUE self, VALUE new_handler) {
652
+ iodine_pubsub_sub_s *s = iodine_pubsub_sub_get(self);
653
+ if (!IODINE_STORE_IS_SKIP(new_handler) &&
654
+ !rb_respond_to(new_handler, rb_intern2("call", 4)))
655
+ rb_raise(rb_eArgError, "handler must respond to `call`");
656
+ s->handler = new_handler;
657
+ return new_handler;
658
+ }
659
+
660
+ /**
661
+ * Returns true if the subscription is still active.
662
+ *
663
+ * @return [Boolean] true if active, false if cancelled or not yet subscribed
664
+ *
665
+ * Ruby: sub.active? # => true/false
666
+ */
667
+ static VALUE iodine_pubsub_sub_active_p(VALUE self) {
668
+ return iodine_pubsub_sub_get(self)->handle ? Qtrue : Qfalse;
669
+ }
670
+
671
+ /**
672
+ * Cancels the subscription early. Idempotent — safe to call multiple times.
673
+ *
674
+ * After cancellation, no further messages will be delivered. The handler
675
+ * proc is NOT released here (it remains accessible via #handler until GC).
676
+ *
677
+ * @return [self]
678
+ *
679
+ * Ruby: sub.cancel # => self
680
+ */
681
+ static VALUE iodine_pubsub_sub_cancel(VALUE self) {
682
+ iodine_pubsub_sub_force_cleanup(iodine_pubsub_sub_get(self));
683
+ return self;
684
+ }
685
+
686
+ /* *****************************************************************************
687
+ Initialize - Ruby Class Registration
688
+ ***************************************************************************** */
689
+
690
+ /**
691
+ * Initializes the Iodine::PubSub::Subscription Ruby class.
692
+ *
693
+ * Defines:
694
+ * - Iodine::PubSub::Subscription class with initialize, handler, handler=,
695
+ * active?, and cancel methods.
696
+ *
697
+ * Each Subscription instance is an independent non-IO-bound subscription,
698
+ * allowing multiple simultaneous subscriptions to the same channel.
699
+ */
700
+ static void Init_Iodine_PubSub_Subscription(void) {
701
+ iodine_rb_IODINE_PUBSUB_SUB = rb_define_class_under(iodine_rb_IODINE_PUBSUB,
702
+ "Subscription",
703
+ rb_cObject);
704
+ STORE.hold(iodine_rb_IODINE_PUBSUB_SUB);
705
+ rb_define_alloc_func(iodine_rb_IODINE_PUBSUB_SUB, iodine_pubsub_sub_alloc);
706
+
707
+ rb_define_method(iodine_rb_IODINE_PUBSUB_SUB,
708
+ "initialize",
709
+ iodine_pubsub_sub_initialize,
710
+ -1);
711
+ rb_define_method(iodine_rb_IODINE_PUBSUB_SUB,
712
+ "handler",
713
+ iodine_pubsub_sub_handler_get,
714
+ 0);
715
+ rb_define_method(iodine_rb_IODINE_PUBSUB_SUB,
716
+ "handler=",
717
+ iodine_pubsub_sub_handler_set,
718
+ 1);
719
+ rb_define_method(iodine_rb_IODINE_PUBSUB_SUB,
720
+ "active?",
721
+ iodine_pubsub_sub_active_p,
722
+ 0);
723
+ rb_define_method(iodine_rb_IODINE_PUBSUB_SUB,
724
+ "cancel",
725
+ iodine_pubsub_sub_cancel,
726
+ 0);
727
+ }
728
+
729
+ #endif /* H___IODINE_PUBSUB_ENG___H */