rage-iodine 5.2.1 → 5.3.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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +28 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile +2 -0
- data/README.md +17 -17
- data/SECURITY.md +32 -0
- data/ext/iodine/extconf.rb +10 -0
- data/ext/iodine/fio.c +117 -5
- data/ext/iodine/fio.h +12 -14
- data/ext/iodine/fio_json_parser.h +5 -4
- data/ext/iodine/fio_tls_openssl.c +140 -90
- data/ext/iodine/fiobj_data.c +13 -11
- data/ext/iodine/fiobj_str.h +1 -1
- data/ext/iodine/fiobject.h +5 -4
- data/ext/iodine/iodine.c +3 -0
- data/ext/iodine/iodine.h +1 -0
- data/ext/iodine/iodine_caller.c +25 -13
- data/ext/iodine/iodine_http.c +2 -1
- data/ext/iodine/iodine_store.c +24 -18
- data/ext/iodine/iodine_worker_pool.c +569 -0
- data/ext/iodine/iodine_worker_pool.h +19 -0
- data/ext/iodine/iodine_worker_pool_test.c +145 -0
- data/ext/iodine/iodine_worker_pool_test.h +19 -0
- data/lib/iodine/version.rb +1 -1
- data/lib/rack/handler/iodine.rb +11 -2
- metadata +9 -3
data/ext/iodine/iodine_store.c
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
#include "iodine.h"
|
|
2
|
-
|
|
3
1
|
#include "iodine_store.h"
|
|
4
2
|
|
|
5
3
|
#include <inttypes.h>
|
|
6
4
|
#include <stdint.h>
|
|
7
5
|
|
|
8
|
-
#
|
|
6
|
+
#include "iodine.h"
|
|
7
|
+
|
|
8
|
+
#define FIO_SET_NAME fio_store
|
|
9
9
|
#define FIO_SET_OBJ_TYPE uintptr_t
|
|
10
10
|
#include <fio.h>
|
|
11
11
|
|
|
@@ -23,13 +23,11 @@ API
|
|
|
23
23
|
|
|
24
24
|
/** Adds an object to the storage (or increases it's reference count). */
|
|
25
25
|
static VALUE storage_add(VALUE obj) {
|
|
26
|
-
if (!obj || obj == Qnil || obj == Qtrue || obj == Qfalse)
|
|
27
|
-
return obj;
|
|
26
|
+
if (!obj || obj == Qnil || obj == Qtrue || obj == Qfalse) return obj;
|
|
28
27
|
uintptr_t old = 0;
|
|
29
28
|
fio_lock(&iodine_storage_lock);
|
|
30
29
|
fio_store_overwrite(&iodine_storage, obj, 1, &old);
|
|
31
|
-
if (old)
|
|
32
|
-
fio_store_overwrite(&iodine_storage, obj, old + 1, NULL);
|
|
30
|
+
if (old) fio_store_overwrite(&iodine_storage, obj, old + 1, NULL);
|
|
33
31
|
if (iodine_storage_count_max < fio_store_count(&iodine_storage))
|
|
34
32
|
iodine_storage_count_max = fio_store_count(&iodine_storage);
|
|
35
33
|
fio_unlock(&iodine_storage_lock);
|
|
@@ -43,8 +41,7 @@ static VALUE storage_remove(VALUE obj) {
|
|
|
43
41
|
fio_lock(&iodine_storage_lock);
|
|
44
42
|
uintptr_t old = 0;
|
|
45
43
|
fio_store_remove(&iodine_storage, obj, 0, &old);
|
|
46
|
-
if (old > 1)
|
|
47
|
-
fio_store_overwrite(&iodine_storage, obj, old - 1, NULL);
|
|
44
|
+
if (old > 1) fio_store_overwrite(&iodine_storage, obj, old - 1, NULL);
|
|
48
45
|
fio_unlock(&iodine_storage_lock);
|
|
49
46
|
return obj;
|
|
50
47
|
}
|
|
@@ -58,15 +55,22 @@ static void storage_print(void) {
|
|
|
58
55
|
uintptr_t index = 0;
|
|
59
56
|
FIO_SET_FOR_LOOP(&iodine_storage, pos) {
|
|
60
57
|
if (pos->obj) {
|
|
61
|
-
fprintf(stderr,
|
|
62
|
-
|
|
58
|
+
fprintf(stderr,
|
|
59
|
+
"[%" PRIuPTR "] => %" PRIuPTR " X obj %p type %d\n",
|
|
60
|
+
index++,
|
|
61
|
+
pos->obj,
|
|
62
|
+
(void *)pos->hash,
|
|
63
|
+
TYPE(pos->hash));
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
fprintf(stderr, "Total of %" PRIuPTR " objects protected form GC\n", index);
|
|
66
67
|
fprintf(stderr,
|
|
67
|
-
"Storage uses %" PRIuPTR " Hash bins for %" PRIuPTR
|
|
68
|
+
"Storage uses %" PRIuPTR " Hash bins for %" PRIuPTR
|
|
69
|
+
" objects\n"
|
|
68
70
|
"The largest collection was %zu objects.\n",
|
|
69
|
-
iodine_storage.capa,
|
|
71
|
+
iodine_storage.capa,
|
|
72
|
+
iodine_storage.count,
|
|
73
|
+
iodine_storage_count_max);
|
|
70
74
|
fio_unlock(&iodine_storage_lock);
|
|
71
75
|
}
|
|
72
76
|
|
|
@@ -85,8 +89,7 @@ GC protection
|
|
|
85
89
|
/* a callback for the GC (marking active objects) */
|
|
86
90
|
static void storage_mark(void *ignore) {
|
|
87
91
|
(void)ignore;
|
|
88
|
-
if (FIO_LOG_LEVEL >= FIO_LOG_LEVEL_DEBUG)
|
|
89
|
-
storage_print();
|
|
92
|
+
if (FIO_LOG_LEVEL >= FIO_LOG_LEVEL_DEBUG) storage_print();
|
|
90
93
|
fio_lock(&iodine_storage_lock);
|
|
91
94
|
// fio_store_compact(&iodine_storage);
|
|
92
95
|
FIO_SET_FOR_LOOP(&iodine_storage, pos) {
|
|
@@ -133,9 +136,12 @@ void iodine_storage_init(void) {
|
|
|
133
136
|
fio_store_capa_require(&iodine_storage, 512);
|
|
134
137
|
VALUE tmp = rb_define_class_under(rb_cObject, "IodineObjectStorage", rb_cObject);
|
|
135
138
|
rb_undef_alloc_func(tmp);
|
|
136
|
-
VALUE storage_obj =
|
|
139
|
+
VALUE storage_obj =
|
|
140
|
+
TypedData_Wrap_Struct(tmp, &storage_type_struct, &iodine_storage);
|
|
137
141
|
// rb_global_variable(&iodine_storage_obj);
|
|
138
142
|
rb_ivar_set(IodineModule, rb_intern2("storage", 7), storage_obj);
|
|
139
|
-
rb_define_module_function(IodineBaseModule,
|
|
140
|
-
|
|
143
|
+
rb_define_module_function(IodineBaseModule,
|
|
144
|
+
"db_print_protected_objects",
|
|
145
|
+
storage_print_rb,
|
|
146
|
+
0);
|
|
141
147
|
}
|
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Iodine WorkerPool - A thread pool for executing blocking operations
|
|
3
|
+
without holding the GVL, used by Ruby's Fiber Scheduler.
|
|
4
|
+
|
|
5
|
+
Based on io-event worker_pool.c by Samuel Williams.
|
|
6
|
+
|
|
7
|
+
Requires Ruby 4.0+ for blocking_operation APIs.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
#include "iodine.h"
|
|
11
|
+
|
|
12
|
+
#ifdef HAVE_IODINE_WORKER_POOL
|
|
13
|
+
|
|
14
|
+
#include "iodine_store.h"
|
|
15
|
+
#include "iodine_worker_pool_test.h"
|
|
16
|
+
|
|
17
|
+
#include <ruby/thread.h>
|
|
18
|
+
#include <ruby/fiber/scheduler.h>
|
|
19
|
+
|
|
20
|
+
#include <pthread.h>
|
|
21
|
+
#include <stdbool.h>
|
|
22
|
+
|
|
23
|
+
static ID call_id;
|
|
24
|
+
static ID workers_id;
|
|
25
|
+
static ID queued_id;
|
|
26
|
+
static ID submitted_id;
|
|
27
|
+
static ID in_progress_id;
|
|
28
|
+
static ID completed_id;
|
|
29
|
+
static ID closed_id;
|
|
30
|
+
|
|
31
|
+
/* *****************************************************************************
|
|
32
|
+
Data Structures
|
|
33
|
+
***************************************************************************** */
|
|
34
|
+
|
|
35
|
+
struct iodine_worker_pool_worker;
|
|
36
|
+
struct iodine_worker_pool_work;
|
|
37
|
+
struct iodine_worker_pool;
|
|
38
|
+
|
|
39
|
+
struct iodine_worker_pool_worker {
|
|
40
|
+
VALUE thread;
|
|
41
|
+
bool interrupted;
|
|
42
|
+
rb_fiber_scheduler_blocking_operation_t *current_op;
|
|
43
|
+
struct iodine_worker_pool *pool;
|
|
44
|
+
struct iodine_worker_pool_worker *next;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
struct iodine_worker_pool_work {
|
|
48
|
+
rb_fiber_scheduler_blocking_operation_t *blocking_operation;
|
|
49
|
+
VALUE callback; /* Block to call when complete */
|
|
50
|
+
struct iodine_worker_pool_work *next;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
struct iodine_worker_pool {
|
|
54
|
+
pthread_mutex_t mutex;
|
|
55
|
+
pthread_cond_t work_available;
|
|
56
|
+
|
|
57
|
+
/* Pending work queue */
|
|
58
|
+
struct iodine_worker_pool_work *work_head;
|
|
59
|
+
struct iodine_worker_pool_work *work_tail;
|
|
60
|
+
|
|
61
|
+
struct iodine_worker_pool_worker *workers;
|
|
62
|
+
size_t worker_count;
|
|
63
|
+
size_t max_workers;
|
|
64
|
+
volatile size_t submitted_count; /* Total tasks enqueued */
|
|
65
|
+
volatile size_t in_progress_count; /* Tasks currently being executed */
|
|
66
|
+
volatile size_t completed_count; /* Total tasks completed */
|
|
67
|
+
|
|
68
|
+
bool initialized;
|
|
69
|
+
bool shutdown;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
static VALUE WorkerPoolKlass;
|
|
73
|
+
|
|
74
|
+
/* *****************************************************************************
|
|
75
|
+
Queue Operations
|
|
76
|
+
***************************************************************************** */
|
|
77
|
+
|
|
78
|
+
static void enqueue_work(struct iodine_worker_pool *pool,
|
|
79
|
+
struct iodine_worker_pool_work *work) {
|
|
80
|
+
if (pool->work_tail) {
|
|
81
|
+
pool->work_tail->next = work;
|
|
82
|
+
} else {
|
|
83
|
+
pool->work_head = work;
|
|
84
|
+
}
|
|
85
|
+
pool->work_tail = work;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static struct iodine_worker_pool_work *dequeue_work(struct iodine_worker_pool *pool) {
|
|
89
|
+
struct iodine_worker_pool_work *work = pool->work_head;
|
|
90
|
+
if (work) {
|
|
91
|
+
pool->work_head = work->next;
|
|
92
|
+
if (!pool->work_head) {
|
|
93
|
+
pool->work_tail = NULL;
|
|
94
|
+
}
|
|
95
|
+
work->next = NULL;
|
|
96
|
+
}
|
|
97
|
+
return work;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* *****************************************************************************
|
|
101
|
+
Worker Thread Functions
|
|
102
|
+
***************************************************************************** */
|
|
103
|
+
|
|
104
|
+
/* Deferred callback - runs on reactor thread to resume fiber */
|
|
105
|
+
static void worker_pool_deferred_callback(void *arg, void *ignore) {
|
|
106
|
+
struct iodine_worker_pool_work *work = arg;
|
|
107
|
+
|
|
108
|
+
IodineCaller.call(work->callback, call_id);
|
|
109
|
+
|
|
110
|
+
IodineStore.remove(work->callback);
|
|
111
|
+
fio_free(work);
|
|
112
|
+
(void)ignore;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Signal completion from worker thread */
|
|
116
|
+
static void worker_pool_complete(struct iodine_worker_pool *pool,
|
|
117
|
+
struct iodine_worker_pool_work *work) {
|
|
118
|
+
fio_atomic_sub(&pool->in_progress_count, 1);
|
|
119
|
+
fio_atomic_add(&pool->completed_count, 1);
|
|
120
|
+
fio_defer(worker_pool_deferred_callback, work, NULL);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* Called to interrupt a blocked worker (e.g., on shutdown or Thread#kill) */
|
|
124
|
+
static void worker_unblock_func(void *_worker) {
|
|
125
|
+
struct iodine_worker_pool_worker *worker = _worker;
|
|
126
|
+
struct iodine_worker_pool *pool = worker->pool;
|
|
127
|
+
|
|
128
|
+
pthread_mutex_lock(&pool->mutex);
|
|
129
|
+
worker->interrupted = true;
|
|
130
|
+
rb_fiber_scheduler_blocking_operation_t *op = worker->current_op;
|
|
131
|
+
pthread_cond_broadcast(&pool->work_available);
|
|
132
|
+
pthread_mutex_unlock(&pool->mutex);
|
|
133
|
+
|
|
134
|
+
/* Cancel outside the lock to avoid potential deadlock
|
|
135
|
+
(cancel may trigger callbacks or signal handlers) */
|
|
136
|
+
if (op) {
|
|
137
|
+
rb_fiber_scheduler_blocking_operation_cancel(op);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#define WORKER_INTERRUPTED ((void *)-1)
|
|
142
|
+
|
|
143
|
+
/* Runs without GVL - waits for work and executes it */
|
|
144
|
+
static void *worker_wait_and_execute(void *_worker) {
|
|
145
|
+
struct iodine_worker_pool_worker *worker = _worker;
|
|
146
|
+
struct iodine_worker_pool *pool = worker->pool;
|
|
147
|
+
|
|
148
|
+
struct iodine_worker_pool_work *work = NULL;
|
|
149
|
+
|
|
150
|
+
pthread_mutex_lock(&pool->mutex);
|
|
151
|
+
|
|
152
|
+
/* Wait for work, shutdown, or interruption */
|
|
153
|
+
while (!pool->work_head && !pool->shutdown && !worker->interrupted) {
|
|
154
|
+
pthread_cond_wait(&pool->work_available, &pool->mutex);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (pool->shutdown) {
|
|
158
|
+
pthread_mutex_unlock(&pool->mutex);
|
|
159
|
+
return NULL; /* Real shutdown signal */
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (worker->interrupted) {
|
|
163
|
+
worker->interrupted = false;
|
|
164
|
+
pthread_mutex_unlock(&pool->mutex);
|
|
165
|
+
return WORKER_INTERRUPTED; /* Soft interrupt */
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
work = dequeue_work(pool);
|
|
169
|
+
|
|
170
|
+
/* Set current_op under the lock so worker_unblock_func can safely read it */
|
|
171
|
+
if (work) {
|
|
172
|
+
fio_atomic_add(&pool->in_progress_count, 1);
|
|
173
|
+
worker->current_op = work->blocking_operation;
|
|
174
|
+
}
|
|
175
|
+
pthread_mutex_unlock(&pool->mutex);
|
|
176
|
+
|
|
177
|
+
if (work) {
|
|
178
|
+
rb_fiber_scheduler_blocking_operation_execute(work->blocking_operation);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
pthread_mutex_lock(&pool->mutex);
|
|
182
|
+
worker->current_op = NULL;
|
|
183
|
+
pthread_mutex_unlock(&pool->mutex);
|
|
184
|
+
|
|
185
|
+
return work;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* Ruby thread entry point */
|
|
189
|
+
static VALUE worker_thread_func(void *_worker) {
|
|
190
|
+
struct iodine_worker_pool_worker *worker = _worker;
|
|
191
|
+
struct iodine_worker_pool *pool = worker->pool;
|
|
192
|
+
|
|
193
|
+
while (true) {
|
|
194
|
+
struct iodine_worker_pool_work *work =
|
|
195
|
+
rb_thread_call_without_gvl(worker_wait_and_execute,
|
|
196
|
+
worker, worker_unblock_func, worker);
|
|
197
|
+
|
|
198
|
+
if (!work) {
|
|
199
|
+
/* Shutdown signal */
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (work == WORKER_INTERRUPTED) {
|
|
204
|
+
/* Soft interrupt occurred - the VM will process signals now that GVL is
|
|
205
|
+
held. Loop back and wait for work again. */
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
worker_pool_complete(pool, work);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return Qnil;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* *****************************************************************************
|
|
216
|
+
Pool Lifecycle
|
|
217
|
+
***************************************************************************** */
|
|
218
|
+
|
|
219
|
+
static void worker_pool_mark(void *ptr) {
|
|
220
|
+
struct iodine_worker_pool *pool = ptr;
|
|
221
|
+
|
|
222
|
+
/* Mark all worker thread objects as movable for GC compaction.
|
|
223
|
+
The workers list is only modified at init and shutdown,
|
|
224
|
+
so it's safe to traverse without a lock during normal operation. */
|
|
225
|
+
struct iodine_worker_pool_worker *worker = pool->workers;
|
|
226
|
+
while (worker) {
|
|
227
|
+
rb_gc_mark_movable(worker->thread);
|
|
228
|
+
worker = worker->next;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* Callbacks are protected via IodineStore */
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
static void worker_pool_compact(void *ptr) {
|
|
235
|
+
struct iodine_worker_pool *pool = ptr;
|
|
236
|
+
|
|
237
|
+
/* Update thread references after GC compaction relocates them */
|
|
238
|
+
struct iodine_worker_pool_worker *worker = pool->workers;
|
|
239
|
+
while (worker) {
|
|
240
|
+
worker->thread = rb_gc_location(worker->thread);
|
|
241
|
+
worker = worker->next;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
static void worker_pool_free(void *ptr) {
|
|
246
|
+
struct iodine_worker_pool *pool = ptr;
|
|
247
|
+
|
|
248
|
+
if (pool->initialized && !pool->shutdown) {
|
|
249
|
+
pthread_mutex_lock(&pool->mutex);
|
|
250
|
+
pool->shutdown = true;
|
|
251
|
+
pthread_cond_broadcast(&pool->work_available);
|
|
252
|
+
pthread_mutex_unlock(&pool->mutex);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* We don't join threads here as it can deadlock during GC.
|
|
256
|
+
Workers will see shutdown flag and exit cleanly. */
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
static size_t worker_pool_size(const void *ptr) {
|
|
260
|
+
return sizeof(struct iodine_worker_pool);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
static const rb_data_type_t iodine_worker_pool_type = {
|
|
264
|
+
.wrap_struct_name = "Iodine::WorkerPool",
|
|
265
|
+
.function =
|
|
266
|
+
{
|
|
267
|
+
.dmark = worker_pool_mark,
|
|
268
|
+
.dfree = worker_pool_free,
|
|
269
|
+
.dsize = worker_pool_size,
|
|
270
|
+
.dcompact = worker_pool_compact,
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
static VALUE worker_pool_allocate(VALUE klass) {
|
|
275
|
+
struct iodine_worker_pool *pool;
|
|
276
|
+
VALUE self = TypedData_Make_Struct(klass, struct iodine_worker_pool,
|
|
277
|
+
&iodine_worker_pool_type, pool);
|
|
278
|
+
memset(pool, 0, sizeof(*pool));
|
|
279
|
+
pool->shutdown = true;
|
|
280
|
+
pool->initialized = false;
|
|
281
|
+
return self;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/* *****************************************************************************
|
|
285
|
+
Ruby Methods
|
|
286
|
+
***************************************************************************** */
|
|
287
|
+
|
|
288
|
+
/* Helper to create a worker thread */
|
|
289
|
+
static int create_worker(VALUE self, struct iodine_worker_pool *pool) {
|
|
290
|
+
if (pool->worker_count >= pool->max_workers) {
|
|
291
|
+
return -1;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
struct iodine_worker_pool_worker *worker = fio_malloc(sizeof(*worker));
|
|
295
|
+
FIO_ASSERT_ALLOC(worker);
|
|
296
|
+
|
|
297
|
+
worker->pool = pool;
|
|
298
|
+
worker->interrupted = false;
|
|
299
|
+
worker->current_op = NULL;
|
|
300
|
+
worker->next = pool->workers;
|
|
301
|
+
|
|
302
|
+
worker->thread = rb_thread_create(worker_thread_func, worker);
|
|
303
|
+
if (NIL_P(worker->thread)) {
|
|
304
|
+
fio_free(worker);
|
|
305
|
+
return -1;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
pool->workers = worker;
|
|
309
|
+
pool->worker_count++;
|
|
310
|
+
|
|
311
|
+
return 0;
|
|
312
|
+
(void)self;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* Forward declaration for use in initialize */
|
|
316
|
+
static VALUE worker_pool_close(VALUE self);
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Iodine::WorkerPool.new(size)
|
|
320
|
+
*
|
|
321
|
+
* Creates a new worker pool with `size` threads for executing
|
|
322
|
+
* blocking operations without holding the GVL.
|
|
323
|
+
*/
|
|
324
|
+
static VALUE worker_pool_initialize(VALUE self, VALUE size) {
|
|
325
|
+
Check_Type(size, T_FIXNUM);
|
|
326
|
+
long requested = NUM2LONG(size);
|
|
327
|
+
|
|
328
|
+
if (requested <= 0) {
|
|
329
|
+
rb_raise(rb_eArgError, "pool size must be greater than 0");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
size_t max_workers = (size_t)requested;
|
|
333
|
+
|
|
334
|
+
struct iodine_worker_pool *pool;
|
|
335
|
+
TypedData_Get_Struct(self, struct iodine_worker_pool,
|
|
336
|
+
&iodine_worker_pool_type, pool);
|
|
337
|
+
|
|
338
|
+
pthread_mutex_init(&pool->mutex, NULL);
|
|
339
|
+
pthread_cond_init(&pool->work_available, NULL);
|
|
340
|
+
pool->initialized = true;
|
|
341
|
+
pool->max_workers = max_workers;
|
|
342
|
+
pool->shutdown = false;
|
|
343
|
+
|
|
344
|
+
IodineStore.add(self);
|
|
345
|
+
|
|
346
|
+
for (size_t i = 0; i < max_workers; i++) {
|
|
347
|
+
if (create_worker(self, pool) != 0) {
|
|
348
|
+
worker_pool_close(self);
|
|
349
|
+
rb_raise(rb_eRuntimeError, "failed to create worker thread %zu", i);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return self;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* pool.enqueue(blocking_operation) { fiber.resume }
|
|
358
|
+
*
|
|
359
|
+
* Enqueues a blocking operation to be executed on a background thread.
|
|
360
|
+
* The block is called (with GVL held) after the operation completes.
|
|
361
|
+
*
|
|
362
|
+
* @param blocking_operation [Object] The blocking operation from Ruby VM
|
|
363
|
+
* @yield Called when the operation completes
|
|
364
|
+
*/
|
|
365
|
+
static VALUE worker_pool_enqueue(VALUE self, VALUE blocking_operation_value) {
|
|
366
|
+
if (!rb_block_given_p()) {
|
|
367
|
+
rb_raise(rb_eArgError, "block required");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
struct iodine_worker_pool *pool;
|
|
371
|
+
TypedData_Get_Struct(self, struct iodine_worker_pool,
|
|
372
|
+
&iodine_worker_pool_type, pool);
|
|
373
|
+
|
|
374
|
+
if (pool->shutdown) {
|
|
375
|
+
rb_raise(rb_eRuntimeError, "Worker pool is shut down");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
rb_fiber_scheduler_blocking_operation_t *blocking_op =
|
|
379
|
+
rb_fiber_scheduler_blocking_operation_extract(blocking_operation_value);
|
|
380
|
+
|
|
381
|
+
if (!blocking_op) {
|
|
382
|
+
rb_raise(rb_eArgError, "Invalid blocking operation");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
VALUE callback = rb_block_proc();
|
|
386
|
+
IodineStore.add(callback);
|
|
387
|
+
|
|
388
|
+
struct iodine_worker_pool_work *work = fio_malloc(sizeof(*work));
|
|
389
|
+
FIO_ASSERT_ALLOC(work);
|
|
390
|
+
|
|
391
|
+
work->blocking_operation = blocking_op;
|
|
392
|
+
work->callback = callback;
|
|
393
|
+
work->next = NULL;
|
|
394
|
+
|
|
395
|
+
/* Enqueue work */
|
|
396
|
+
pthread_mutex_lock(&pool->mutex);
|
|
397
|
+
enqueue_work(pool, work);
|
|
398
|
+
pool->submitted_count++;
|
|
399
|
+
pthread_cond_signal(&pool->work_available);
|
|
400
|
+
pthread_mutex_unlock(&pool->mutex);
|
|
401
|
+
|
|
402
|
+
return Qtrue;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* Helper to count items in the work queue (must hold mutex) */
|
|
406
|
+
static size_t count_queue_size(struct iodine_worker_pool *pool) {
|
|
407
|
+
size_t count = 0;
|
|
408
|
+
struct iodine_worker_pool_work *work = pool->work_head;
|
|
409
|
+
while (work) {
|
|
410
|
+
count++;
|
|
411
|
+
work = work->next;
|
|
412
|
+
}
|
|
413
|
+
return count;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* pool.stats -> Hash
|
|
418
|
+
*
|
|
419
|
+
* Returns statistics about the worker pool.
|
|
420
|
+
*
|
|
421
|
+
* @return [Hash] Statistics hash with the following keys:
|
|
422
|
+
* - :workers [Integer] Current number of worker threads
|
|
423
|
+
* - :queued [Integer] Number of tasks waiting in queue
|
|
424
|
+
* - :submitted [Integer] Total tasks enqueued
|
|
425
|
+
* - :in_progress [Integer] Tasks currently being executed
|
|
426
|
+
* - :completed [Integer] Total tasks completed
|
|
427
|
+
* - :closed [Boolean] Whether the pool is closed
|
|
428
|
+
*/
|
|
429
|
+
static VALUE worker_pool_statistics(VALUE self) {
|
|
430
|
+
struct iodine_worker_pool *pool;
|
|
431
|
+
TypedData_Get_Struct(self, struct iodine_worker_pool,
|
|
432
|
+
&iodine_worker_pool_type, pool);
|
|
433
|
+
|
|
434
|
+
size_t submitted = pool->submitted_count;
|
|
435
|
+
size_t completed = pool->completed_count;
|
|
436
|
+
bool closed = pool->shutdown;
|
|
437
|
+
size_t in_progress = 0;
|
|
438
|
+
size_t workers = 0;
|
|
439
|
+
size_t queued = 0;
|
|
440
|
+
|
|
441
|
+
if (pool->initialized) {
|
|
442
|
+
pthread_mutex_lock(&pool->mutex);
|
|
443
|
+
workers = pool->worker_count;
|
|
444
|
+
in_progress = pool->in_progress_count;
|
|
445
|
+
queued = count_queue_size(pool);
|
|
446
|
+
pthread_mutex_unlock(&pool->mutex);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
VALUE stats = rb_hash_new();
|
|
450
|
+
rb_hash_aset(stats, ID2SYM(workers_id), SIZET2NUM(workers));
|
|
451
|
+
rb_hash_aset(stats, ID2SYM(queued_id), SIZET2NUM(queued));
|
|
452
|
+
rb_hash_aset(stats, ID2SYM(in_progress_id), SIZET2NUM(in_progress));
|
|
453
|
+
rb_hash_aset(stats, ID2SYM(completed_id), SIZET2NUM(completed));
|
|
454
|
+
rb_hash_aset(stats, ID2SYM(submitted_id), SIZET2NUM(submitted));
|
|
455
|
+
rb_hash_aset(stats, ID2SYM(closed_id), closed ? Qtrue : Qfalse);
|
|
456
|
+
|
|
457
|
+
return stats;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* pool.close -> nil
|
|
462
|
+
*
|
|
463
|
+
* Closes the worker pool during process shutdown.
|
|
464
|
+
* Queued work items that have not started are discarded and their
|
|
465
|
+
* callbacks are not invoked. Work already executing is not cancelled;
|
|
466
|
+
* close waits for worker threads to return naturally.
|
|
467
|
+
*
|
|
468
|
+
* @note This is intended for teardown only.
|
|
469
|
+
*/
|
|
470
|
+
static VALUE worker_pool_close(VALUE self) {
|
|
471
|
+
struct iodine_worker_pool *pool;
|
|
472
|
+
TypedData_Get_Struct(self, struct iodine_worker_pool,
|
|
473
|
+
&iodine_worker_pool_type, pool);
|
|
474
|
+
|
|
475
|
+
if (pool->shutdown || !pool->initialized) {
|
|
476
|
+
return Qnil;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
pthread_mutex_lock(&pool->mutex);
|
|
480
|
+
pool->shutdown = true;
|
|
481
|
+
pthread_cond_broadcast(&pool->work_available);
|
|
482
|
+
pthread_mutex_unlock(&pool->mutex);
|
|
483
|
+
|
|
484
|
+
/* Join all worker threads */
|
|
485
|
+
struct iodine_worker_pool_worker *worker = pool->workers;
|
|
486
|
+
while (worker) {
|
|
487
|
+
if (!NIL_P(worker->thread)) {
|
|
488
|
+
rb_funcall(worker->thread, rb_intern("join"), 0);
|
|
489
|
+
}
|
|
490
|
+
worker = worker->next;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/* Free worker structures */
|
|
494
|
+
worker = pool->workers;
|
|
495
|
+
while (worker) {
|
|
496
|
+
struct iodine_worker_pool_worker *next = worker->next;
|
|
497
|
+
fio_free(worker);
|
|
498
|
+
worker = next;
|
|
499
|
+
}
|
|
500
|
+
pool->workers = NULL;
|
|
501
|
+
pool->worker_count = 0;
|
|
502
|
+
|
|
503
|
+
/* Free any remaining queued work */
|
|
504
|
+
struct iodine_worker_pool_work *work = pool->work_head;
|
|
505
|
+
while (work) {
|
|
506
|
+
struct iodine_worker_pool_work *next = work->next;
|
|
507
|
+
IodineStore.remove(work->callback);
|
|
508
|
+
fio_free(work);
|
|
509
|
+
work = next;
|
|
510
|
+
}
|
|
511
|
+
pool->work_head = pool->work_tail = NULL;
|
|
512
|
+
|
|
513
|
+
pthread_mutex_destroy(&pool->mutex);
|
|
514
|
+
pthread_cond_destroy(&pool->work_available);
|
|
515
|
+
pool->initialized = false;
|
|
516
|
+
|
|
517
|
+
IodineStore.remove(self);
|
|
518
|
+
|
|
519
|
+
return Qnil;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* pool.size -> Integer
|
|
524
|
+
*
|
|
525
|
+
* Returns the number of worker threads.
|
|
526
|
+
*/
|
|
527
|
+
static VALUE worker_pool_size_method(VALUE self) {
|
|
528
|
+
struct iodine_worker_pool *pool;
|
|
529
|
+
TypedData_Get_Struct(self, struct iodine_worker_pool,
|
|
530
|
+
&iodine_worker_pool_type, pool);
|
|
531
|
+
return SIZET2NUM(pool->worker_count);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/* *****************************************************************************
|
|
535
|
+
Initialization
|
|
536
|
+
***************************************************************************** */
|
|
537
|
+
|
|
538
|
+
void iodine_worker_pool_init(void) {
|
|
539
|
+
call_id = rb_intern("call");
|
|
540
|
+
|
|
541
|
+
workers_id = rb_intern("workers");
|
|
542
|
+
queued_id = rb_intern("queued");
|
|
543
|
+
submitted_id = rb_intern("submitted");
|
|
544
|
+
in_progress_id = rb_intern("in_progress");
|
|
545
|
+
completed_id = rb_intern("completed");
|
|
546
|
+
closed_id = rb_intern("closed");
|
|
547
|
+
|
|
548
|
+
WorkerPoolKlass = rb_define_class_under(IodineModule, "WorkerPool", rb_cObject);
|
|
549
|
+
|
|
550
|
+
rb_define_alloc_func(WorkerPoolKlass, worker_pool_allocate);
|
|
551
|
+
rb_define_method(WorkerPoolKlass, "initialize", worker_pool_initialize, 1);
|
|
552
|
+
rb_define_method(WorkerPoolKlass, "enqueue", worker_pool_enqueue, 1);
|
|
553
|
+
rb_define_method(WorkerPoolKlass, "close", worker_pool_close, 0);
|
|
554
|
+
rb_define_method(WorkerPoolKlass, "size", worker_pool_size_method, 0);
|
|
555
|
+
rb_define_method(WorkerPoolKlass, "stats", worker_pool_statistics, 0);
|
|
556
|
+
|
|
557
|
+
/* Initialize test helpers */
|
|
558
|
+
iodine_worker_pool_test_init(WorkerPoolKlass);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
#else /* !HAVE_IODINE_WORKER_POOL */
|
|
562
|
+
|
|
563
|
+
/*
|
|
564
|
+
* Stub implementation for Ruby versions without blocking_operation APIs.
|
|
565
|
+
* The WorkerPool class is not available.
|
|
566
|
+
*/
|
|
567
|
+
void iodine_worker_pool_init(void) {}
|
|
568
|
+
|
|
569
|
+
#endif /* HAVE_IODINE_WORKER_POOL */
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Iodine WorkerPool - A thread pool for executing blocking operations
|
|
3
|
+
without holding the GVL, used by Ruby's Fiber Scheduler.
|
|
4
|
+
|
|
5
|
+
Based on io-event worker_pool.c by Samuel Williams.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
#ifndef H_IODINE_WORKER_POOL_H
|
|
9
|
+
#define H_IODINE_WORKER_POOL_H
|
|
10
|
+
|
|
11
|
+
#include <ruby.h>
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Initializes the Iodine::WorkerPool Ruby class.
|
|
15
|
+
* Called from Init_iodine_ext().
|
|
16
|
+
*/
|
|
17
|
+
void iodine_worker_pool_init(void);
|
|
18
|
+
|
|
19
|
+
#endif
|