semian 0.0.3 → 0.0.4
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/.gitignore +1 -0
- data/Gemfile.lock +1 -1
- data/Rakefile +8 -0
- data/ext/semian/extconf.rb +9 -4
- data/ext/semian/semian.c +280 -23
- data/lib/semian/version.rb +1 -1
- data/lib/semian.rb +70 -2
- data/test/test_semian.rb +119 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 04219b7eea123cad23d49905ec52c376484013fb
|
4
|
+
data.tar.gz: 4922f4c4f148425ebb7d1ced40dc8bf6fc3454ec
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 896373f2c1401134888a8ac3d503663bcbec528f7d304173196310ec7a585f3e5c01faa0c92ce199ba935b8324e4fa57a1f177f77503bfcd59219e64f576952e
|
7
|
+
data.tar.gz: ea74197c538ba50e31c01a518570f3bb94f60d5f28be77554616dc9c2595cbf4be24e280cff612d66ef0c970737f025971f93316c2c6a9a3d9a83e1472698e20
|
data/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
data/Rakefile
CHANGED
@@ -30,3 +30,11 @@ Rake::TestTask.new 'test' do |t|
|
|
30
30
|
t.test_files = FileList['test/test_*.rb']
|
31
31
|
end
|
32
32
|
task :test => :build
|
33
|
+
|
34
|
+
# ==========================================================
|
35
|
+
# Documentation
|
36
|
+
# ==========================================================
|
37
|
+
require 'rdoc/task'
|
38
|
+
RDoc::Task.new do |rdoc|
|
39
|
+
rdoc.rdoc_files.include("lib/*.rb", "ext/semian/*.c")
|
40
|
+
end
|
data/ext/semian/extconf.rb
CHANGED
@@ -1,9 +1,7 @@
|
|
1
1
|
require 'mkmf'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
abort 'openssl is missing. please install openssl.' unless find_header('openssl/md5.h')
|
6
|
-
abort 'openssl is missing. please install openssl.' unless find_library('crypto', 'MD5_Init')
|
3
|
+
abort 'openssl is missing. please install openssl.' unless find_header('openssl/sha.h')
|
4
|
+
abort 'openssl is missing. please install openssl.' unless find_library('crypto', 'SHA1')
|
7
5
|
|
8
6
|
have_header 'sys/ipc.h'
|
9
7
|
have_header 'sys/sem.h'
|
@@ -12,4 +10,11 @@ have_header 'sys/types.h'
|
|
12
10
|
have_func 'rb_thread_blocking_region'
|
13
11
|
have_func 'rb_thread_call_without_gvl'
|
14
12
|
|
13
|
+
$CFLAGS = "-D_GNU_SOURCE -Werror -Wall "
|
14
|
+
if ENV.has_key?('DEBUG')
|
15
|
+
$CFLAGS << "-O0 -g"
|
16
|
+
else
|
17
|
+
$CFLAGS << "-O3"
|
18
|
+
end
|
19
|
+
|
15
20
|
create_makefile('semian/semian')
|
data/ext/semian/semian.c
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
#include <sys/types.h>
|
2
2
|
#include <sys/ipc.h>
|
3
3
|
#include <sys/sem.h>
|
4
|
+
#include <sys/time.h>
|
4
5
|
#include <errno.h>
|
5
6
|
#include <string.h>
|
6
7
|
|
@@ -12,6 +13,14 @@
|
|
12
13
|
|
13
14
|
#include <stdio.h>
|
14
15
|
|
16
|
+
union semun {
|
17
|
+
int val; /* Value for SETVAL */
|
18
|
+
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
|
19
|
+
unsigned short *array; /* Array for GETALL, SETALL */
|
20
|
+
struct seminfo *__buf; /* Buffer for IPC_INFO
|
21
|
+
(Linux-specific) */
|
22
|
+
};
|
23
|
+
|
15
24
|
#if defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL) && defined(HAVE_RUBY_THREAD_H)
|
16
25
|
// 2.0
|
17
26
|
#include <ruby/thread.h>
|
@@ -23,7 +32,14 @@ typedef VALUE (*my_blocking_fn_t)(void*);
|
|
23
32
|
#endif
|
24
33
|
|
25
34
|
static ID id_timeout;
|
26
|
-
static VALUE eSyscall, eTimeout;
|
35
|
+
static VALUE eSyscall, eTimeout, eInternal;
|
36
|
+
static int system_max_sempahore_count;
|
37
|
+
|
38
|
+
static const int kIndexTickets = 0;
|
39
|
+
static const int kIndexTicketMax = 1;
|
40
|
+
static const int kIndexLock = 2;
|
41
|
+
|
42
|
+
static const int kNumSemaphores = 3;
|
27
43
|
|
28
44
|
typedef struct {
|
29
45
|
int sem_id;
|
@@ -35,10 +51,13 @@ typedef struct {
|
|
35
51
|
static key_t
|
36
52
|
generate_key(const char *name)
|
37
53
|
{
|
38
|
-
|
39
|
-
|
54
|
+
union {
|
55
|
+
unsigned char str[SHA_DIGEST_LENGTH];
|
56
|
+
key_t key;
|
57
|
+
} digest;
|
58
|
+
SHA1((const unsigned char *) name, strlen(name), digest.str);
|
40
59
|
/* TODO: compile-time assertion that sizeof(key_t) > SHA_DIGEST_LENGTH */
|
41
|
-
return
|
60
|
+
return digest.key;
|
42
61
|
}
|
43
62
|
|
44
63
|
static void
|
@@ -96,11 +115,168 @@ semian_resource_alloc(VALUE klass)
|
|
96
115
|
return obj;
|
97
116
|
}
|
98
117
|
|
118
|
+
static void
|
119
|
+
set_sempahore_permissions(int sem_id, int permissions)
|
120
|
+
{
|
121
|
+
union semun sem_opts;
|
122
|
+
struct semid_ds stat_buf;
|
123
|
+
|
124
|
+
sem_opts.buf = &stat_buf;
|
125
|
+
semctl(sem_id, 0, IPC_STAT, sem_opts);
|
126
|
+
if ((stat_buf.sem_perm.mode & 0xfff) != permissions) {
|
127
|
+
stat_buf.sem_perm.mode &= ~0xfff;
|
128
|
+
stat_buf.sem_perm.mode |= permissions;
|
129
|
+
semctl(sem_id, 0, IPC_SET, sem_opts);
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
static const int kInternalTimeout = 5; /* seconds */
|
134
|
+
|
135
|
+
static int
|
136
|
+
get_max_tickets(int sem_id)
|
137
|
+
{
|
138
|
+
int ret = semctl(sem_id, kIndexTicketMax, GETVAL);
|
139
|
+
if (ret == -1) {
|
140
|
+
rb_raise(eInternal, "error getting max ticket count, errno: %d (%s)", errno, strerror(errno));
|
141
|
+
}
|
142
|
+
return ret;
|
143
|
+
}
|
144
|
+
|
145
|
+
static int
|
146
|
+
perform_semop(int sem_id, short index, short op, short flags, struct timespec *ts)
|
147
|
+
{
|
148
|
+
struct sembuf buf = { 0 };
|
149
|
+
|
150
|
+
buf.sem_num = index;
|
151
|
+
buf.sem_op = op;
|
152
|
+
buf.sem_flg = flags;
|
153
|
+
|
154
|
+
if (ts) {
|
155
|
+
return semtimedop(sem_id, &buf, 1, ts);
|
156
|
+
} else {
|
157
|
+
return semop(sem_id, &buf, 1);
|
158
|
+
}
|
159
|
+
}
|
160
|
+
|
161
|
+
typedef struct {
|
162
|
+
int sem_id;
|
163
|
+
int tickets;
|
164
|
+
} update_ticket_count_t;
|
165
|
+
|
166
|
+
static VALUE
|
167
|
+
update_ticket_count(update_ticket_count_t *tc)
|
168
|
+
{
|
169
|
+
short delta;
|
170
|
+
struct timespec ts = { 0 };
|
171
|
+
ts.tv_sec = kInternalTimeout;
|
172
|
+
|
173
|
+
if (get_max_tickets(tc->sem_id) != tc->tickets) {
|
174
|
+
delta = tc->tickets - get_max_tickets(tc->sem_id);
|
175
|
+
|
176
|
+
if (perform_semop(tc->sem_id, kIndexTickets, delta, 0, &ts) == -1) {
|
177
|
+
rb_raise(eInternal, "error setting ticket count, errno: %d (%s)", errno, strerror(errno));
|
178
|
+
}
|
179
|
+
|
180
|
+
if (semctl(tc->sem_id, kIndexTicketMax, SETVAL, tc->tickets) == -1) {
|
181
|
+
rb_raise(eInternal, "error updating max ticket count, errno: %d (%s)", errno, strerror(errno));
|
182
|
+
}
|
183
|
+
}
|
184
|
+
|
185
|
+
return Qnil;
|
186
|
+
}
|
187
|
+
|
188
|
+
static void
|
189
|
+
configure_tickets(int sem_id, int tickets, int should_initialize)
|
190
|
+
{
|
191
|
+
struct timespec ts = { 0 };
|
192
|
+
unsigned short init_vals[kNumSemaphores];
|
193
|
+
struct timeval start_time, cur_time;
|
194
|
+
update_ticket_count_t tc;
|
195
|
+
int state;
|
196
|
+
|
197
|
+
if (should_initialize) {
|
198
|
+
init_vals[kIndexTickets] = init_vals[kIndexTicketMax] = tickets;
|
199
|
+
init_vals[kIndexLock] = 1;
|
200
|
+
if (semctl(sem_id, 0, SETALL, init_vals) == -1) {
|
201
|
+
raise_semian_syscall_error("semctl()", errno);
|
202
|
+
}
|
203
|
+
} else if (tickets > 0) {
|
204
|
+
/* it's possible that we haven't actually initialized the
|
205
|
+
sempahore structure yet - wait a bit in that case */
|
206
|
+
if (get_max_tickets(sem_id) == 0) {
|
207
|
+
gettimeofday(&start_time, NULL);
|
208
|
+
while (get_max_tickets(sem_id) == 0) {
|
209
|
+
usleep(10000); /* 10ms */
|
210
|
+
gettimeofday(&cur_time, NULL);
|
211
|
+
if ((cur_time.tv_sec - start_time.tv_sec) > kInternalTimeout) {
|
212
|
+
rb_raise(eInternal, "timeout waiting for semaphore initialization");
|
213
|
+
}
|
214
|
+
}
|
215
|
+
}
|
216
|
+
|
217
|
+
/*
|
218
|
+
If the current max ticket count is not the same as the requested ticket
|
219
|
+
count, we need to resize the count. We do this by adding the delta of
|
220
|
+
(tickets - current_max_tickets) to the semaphore value.
|
221
|
+
*/
|
222
|
+
if (get_max_tickets(sem_id) != tickets) {
|
223
|
+
ts.tv_sec = kInternalTimeout;
|
224
|
+
|
225
|
+
if (perform_semop(sem_id, kIndexLock, -1, SEM_UNDO, &ts) == -1) {
|
226
|
+
raise_semian_syscall_error("error acquiring internal semaphore lock, semtimedop()", errno);
|
227
|
+
}
|
228
|
+
|
229
|
+
tc.sem_id = sem_id;
|
230
|
+
tc.tickets = tickets;
|
231
|
+
rb_protect((VALUE (*)(VALUE)) update_ticket_count, (VALUE) &tc, &state);
|
232
|
+
|
233
|
+
if (perform_semop(sem_id, kIndexLock, 1, SEM_UNDO, NULL) == -1) {
|
234
|
+
raise_semian_syscall_error("error releasing internal semaphore lock, semop()", errno);
|
235
|
+
}
|
236
|
+
|
237
|
+
if (state) {
|
238
|
+
rb_jump_tag(state);
|
239
|
+
}
|
240
|
+
}
|
241
|
+
}
|
242
|
+
}
|
243
|
+
|
244
|
+
static int
|
245
|
+
create_semaphore(int key, int permissions, int *created)
|
246
|
+
{
|
247
|
+
int semid = 0;
|
248
|
+
int flags = 0;
|
249
|
+
|
250
|
+
*created = 0;
|
251
|
+
flags = IPC_EXCL | IPC_CREAT | FIX2LONG(permissions);
|
252
|
+
|
253
|
+
semid = semget(key, kNumSemaphores, flags);
|
254
|
+
if (semid >= 0) {
|
255
|
+
*created = 1;
|
256
|
+
} else if (semid == -1 && errno == EEXIST) {
|
257
|
+
flags &= ~IPC_EXCL;
|
258
|
+
semid = semget(key, kNumSemaphores, flags);
|
259
|
+
}
|
260
|
+
return semid;
|
261
|
+
}
|
262
|
+
|
263
|
+
static int
|
264
|
+
get_semaphore(int key)
|
265
|
+
{
|
266
|
+
return semget(key, kNumSemaphores, 0);
|
267
|
+
}
|
268
|
+
|
269
|
+
/*
|
270
|
+
* call-seq:
|
271
|
+
* Semian::Resource.new(id, tickets, permissions, default_timeout) -> resource
|
272
|
+
*
|
273
|
+
* Creates a new Resource. Do not create resources directly. Use Semian.register.
|
274
|
+
*/
|
99
275
|
static VALUE
|
100
|
-
semian_resource_initialize(VALUE self, VALUE id, VALUE tickets, VALUE default_timeout)
|
276
|
+
semian_resource_initialize(VALUE self, VALUE id, VALUE tickets, VALUE permissions, VALUE default_timeout)
|
101
277
|
{
|
102
278
|
key_t key;
|
103
|
-
int
|
279
|
+
int created = 0;
|
104
280
|
semian_resource_t *res = NULL;
|
105
281
|
const char *id_str = NULL;
|
106
282
|
|
@@ -108,11 +284,12 @@ semian_resource_initialize(VALUE self, VALUE id, VALUE tickets, VALUE default_ti
|
|
108
284
|
rb_raise(rb_eTypeError, "id must be a symbol or string");
|
109
285
|
}
|
110
286
|
Check_Type(tickets, T_FIXNUM);
|
287
|
+
Check_Type(permissions, T_FIXNUM);
|
111
288
|
if (TYPE(default_timeout) != T_FIXNUM && TYPE(default_timeout) != T_FLOAT) {
|
112
289
|
rb_raise(rb_eTypeError, "expected numeric type for default_timeout");
|
113
290
|
}
|
114
|
-
if (FIX2LONG(tickets) < 0) {
|
115
|
-
rb_raise(rb_eArgError, "ticket count must be a non-negative value");
|
291
|
+
if (FIX2LONG(tickets) < 0 || FIX2LONG(tickets) > system_max_sempahore_count) {
|
292
|
+
rb_raise(rb_eArgError, "ticket count must be a non-negative value and less than %d", system_max_sempahore_count);
|
116
293
|
}
|
117
294
|
if (NUM2DBL(default_timeout) < 0) {
|
118
295
|
rb_raise(rb_eArgError, "default timeout must be non-negative value");
|
@@ -128,19 +305,14 @@ semian_resource_initialize(VALUE self, VALUE id, VALUE tickets, VALUE default_ti
|
|
128
305
|
ms_to_timespec(NUM2DBL(default_timeout) * 1000, &res->timeout);
|
129
306
|
res->name = strdup(id_str);
|
130
307
|
|
131
|
-
|
132
|
-
flags |= IPC_CREAT;
|
133
|
-
}
|
134
|
-
|
135
|
-
res->sem_id = semget(key, 1, flags);
|
308
|
+
res->sem_id = FIX2LONG(tickets) == 0 ? get_semaphore(key) : create_semaphore(key, permissions, &created);
|
136
309
|
if (res->sem_id == -1) {
|
137
310
|
raise_semian_syscall_error("semget()", errno);
|
138
311
|
}
|
139
312
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
}
|
313
|
+
configure_tickets(res->sem_id, FIX2LONG(tickets), created);
|
314
|
+
|
315
|
+
set_sempahore_permissions(res->sem_id, FIX2LONG(permissions));
|
144
316
|
|
145
317
|
return self;
|
146
318
|
}
|
@@ -150,8 +322,7 @@ cleanup_semian_resource_acquire(VALUE self)
|
|
150
322
|
{
|
151
323
|
semian_resource_t *res = NULL;
|
152
324
|
TypedData_Get_Struct(self, semian_resource_t, &semian_resource_type, res);
|
153
|
-
|
154
|
-
if (semop(res->sem_id, &buf, 1) == -1) {
|
325
|
+
if (perform_semop(res->sem_id, kIndexTickets, 1, SEM_UNDO, NULL) == -1) {
|
155
326
|
res->error = errno;
|
156
327
|
}
|
157
328
|
return Qnil;
|
@@ -161,14 +332,24 @@ static void *
|
|
161
332
|
acquire_sempahore_without_gvl(void *p)
|
162
333
|
{
|
163
334
|
semian_resource_t *res = (semian_resource_t *) p;
|
164
|
-
struct sembuf buf = { 0, -1, SEM_UNDO };
|
165
335
|
res->error = 0;
|
166
|
-
if (
|
336
|
+
if (perform_semop(res->sem_id, kIndexTickets, -1, SEM_UNDO, &res->timeout) == -1) {
|
167
337
|
res->error = errno;
|
168
338
|
}
|
169
339
|
return NULL;
|
170
340
|
}
|
171
341
|
|
342
|
+
/*
|
343
|
+
* call-seq:
|
344
|
+
* resource.acquire(timeout: default_timeout) { ... } -> result of the block
|
345
|
+
*
|
346
|
+
* Acquires a resource. The call will block for <code>timeout</code> seconds if a ticket
|
347
|
+
* is not available. If no ticket is available within the timeout period, Semian::TimeoutError
|
348
|
+
* will be raised.
|
349
|
+
*
|
350
|
+
* If no timeout argument is provided, the default timeout passed to Semian.register will be used.
|
351
|
+
*
|
352
|
+
*/
|
172
353
|
static VALUE
|
173
354
|
semian_resource_acquire(int argc, VALUE *argv, VALUE self)
|
174
355
|
{
|
@@ -208,6 +389,16 @@ semian_resource_acquire(int argc, VALUE *argv, VALUE self)
|
|
208
389
|
return rb_ensure(rb_yield, self, cleanup_semian_resource_acquire, self);
|
209
390
|
}
|
210
391
|
|
392
|
+
/*
|
393
|
+
* call-seq:
|
394
|
+
* resource.destroy() -> true
|
395
|
+
*
|
396
|
+
* Destroys a resource. This method will destroy the underlying SysV semaphore.
|
397
|
+
* If there is any code in other threads or processes blocking or using the resource
|
398
|
+
* they will likely raise.
|
399
|
+
*
|
400
|
+
* Use this method very carefully.
|
401
|
+
*/
|
211
402
|
static VALUE
|
212
403
|
semian_resource_destroy(VALUE self)
|
213
404
|
{
|
@@ -221,6 +412,12 @@ semian_resource_destroy(VALUE self)
|
|
221
412
|
return Qtrue;
|
222
413
|
}
|
223
414
|
|
415
|
+
/*
|
416
|
+
* call-seq:
|
417
|
+
* resource.count -> count
|
418
|
+
*
|
419
|
+
* Returns the current ticket count for a resource.
|
420
|
+
*/
|
224
421
|
static VALUE
|
225
422
|
semian_resource_count(VALUE self)
|
226
423
|
{
|
@@ -236,21 +433,81 @@ semian_resource_count(VALUE self)
|
|
236
433
|
return LONG2FIX(ret);
|
237
434
|
}
|
238
435
|
|
436
|
+
/*
|
437
|
+
* call-seq:
|
438
|
+
* resource.semid -> id
|
439
|
+
*
|
440
|
+
* Returns the SysV semaphore id of a resource.
|
441
|
+
*/
|
442
|
+
static VALUE
|
443
|
+
semian_resource_id(VALUE self)
|
444
|
+
{
|
445
|
+
semian_resource_t *res = NULL;
|
446
|
+
TypedData_Get_Struct(self, semian_resource_t, &semian_resource_type, res);
|
447
|
+
return LONG2FIX(res->sem_id);
|
448
|
+
}
|
449
|
+
|
239
450
|
void Init_semian()
|
240
451
|
{
|
241
452
|
VALUE cSemian, cResource, eBaseError;
|
453
|
+
struct seminfo info_buf;
|
242
454
|
|
243
455
|
cSemian = rb_define_class("Semian", rb_cObject);
|
244
|
-
|
456
|
+
|
457
|
+
/*
|
458
|
+
* Document-class: Semian::Resource
|
459
|
+
*
|
460
|
+
* Resource is the fundamental class of Semian. It is essentially a wrapper around a
|
461
|
+
* SystemV semaphore.
|
462
|
+
*
|
463
|
+
* You should not create this class directly, it will be created indirectly via Semian.register.
|
464
|
+
*/
|
465
|
+
cResource = rb_define_class_under(cSemian, "Resource", rb_cObject);
|
466
|
+
|
467
|
+
/* Document-class: Semian::BaseError
|
468
|
+
*
|
469
|
+
* Base error class for all other Semian errors.
|
470
|
+
*/
|
245
471
|
eBaseError = rb_define_class_under(cSemian, "BaseError", rb_eStandardError);
|
472
|
+
|
473
|
+
/* Document-class: Semian::SyscallError
|
474
|
+
*
|
475
|
+
* Represents a Semian error that was caused by an underlying syscall failure.
|
476
|
+
*/
|
246
477
|
eSyscall = rb_define_class_under(cSemian, "SyscallError", eBaseError);
|
478
|
+
|
479
|
+
/* Document-class: Semian::TimeoutError
|
480
|
+
*
|
481
|
+
* Raised when a Semian operation timed out.
|
482
|
+
*/
|
247
483
|
eTimeout = rb_define_class_under(cSemian, "TimeoutError", eBaseError);
|
248
484
|
|
485
|
+
/* Document-class: Semian::InternalError
|
486
|
+
*
|
487
|
+
* An internal Semian error. These errors should be typically never be raised. If
|
488
|
+
* they do, there's a high likelyhood that the underlying SysV semaphore set
|
489
|
+
* has been corrupted.
|
490
|
+
*
|
491
|
+
* If this happens, a strong course of action would be to delete the semaphores
|
492
|
+
* using the <code>ipcrm</code> command line tool. Semian will re-initialize
|
493
|
+
* the semaphore in this case.
|
494
|
+
*/
|
495
|
+
eInternal = rb_define_class_under(cSemian, "InternalError", eBaseError);
|
496
|
+
|
249
497
|
rb_define_alloc_func(cResource, semian_resource_alloc);
|
250
|
-
rb_define_method(cResource, "initialize", semian_resource_initialize,
|
498
|
+
rb_define_method(cResource, "initialize", semian_resource_initialize, 4);
|
251
499
|
rb_define_method(cResource, "acquire", semian_resource_acquire, -1);
|
252
500
|
rb_define_method(cResource, "count", semian_resource_count, 0);
|
501
|
+
rb_define_method(cResource, "semid", semian_resource_id, 0);
|
253
502
|
rb_define_method(cResource, "destroy", semian_resource_destroy, 0);
|
254
503
|
|
255
504
|
id_timeout = rb_intern("timeout");
|
505
|
+
|
506
|
+
if (semctl(0, 0, SEM_INFO, &info_buf) == -1) {
|
507
|
+
rb_raise(eInternal, "unable to determine maximum sempahore count - semctl() returned %d: %s ", errno, strerror(errno));
|
508
|
+
}
|
509
|
+
system_max_sempahore_count = info_buf.semvmx;
|
510
|
+
|
511
|
+
/* Maximum number of tickets available on this system. */
|
512
|
+
rb_define_const(cSemian, "MAX_TICKETS", INT2FIX(system_max_sempahore_count));
|
256
513
|
}
|
data/lib/semian/version.rb
CHANGED
data/lib/semian.rb
CHANGED
@@ -1,16 +1,84 @@
|
|
1
1
|
require 'semian/semian'
|
2
2
|
|
3
|
+
#
|
4
|
+
# === Overview
|
5
|
+
#
|
6
|
+
# Semian is a library that can be used to control access to external services.
|
7
|
+
#
|
8
|
+
# It's desirable to control access to external services so that in the case that one
|
9
|
+
# is slow or not responding, the performance of an entire system is not compromised.
|
10
|
+
#
|
11
|
+
# Semian uses the concept of a "resource" as an identifier that controls access
|
12
|
+
# to some external service. So for example, "mysql" or "redis" would be considered
|
13
|
+
# resources. If a system is sharded, like a database, you would typically create
|
14
|
+
# a resource for every shard.
|
15
|
+
#
|
16
|
+
# Resources are visible across an IPC namespace. This means that you can register a
|
17
|
+
# resource in one process and access it from another. This is useful in application
|
18
|
+
# servers like Unicorn that are multi-process. A resource is persistent. It will
|
19
|
+
# continue to exist even after the application exits, and will only be destroyed by
|
20
|
+
# manually removing it with the <code>ipcrm</code> command, calling Resource.destroy,
|
21
|
+
# or rebooting the machine.
|
22
|
+
#
|
23
|
+
# Each resource has a configurable number of tickets. Tickets are what controls
|
24
|
+
# access to the external service. If a client does not have a ticket, it cannot access
|
25
|
+
# a service. If there are no tickets available, the client will block for a configurable
|
26
|
+
# amount of time until a ticket is available. If there are no tickets available after
|
27
|
+
# the timeout period has elapsed, the client will be unable to access the service and
|
28
|
+
# an error will be raised.
|
29
|
+
#
|
30
|
+
# A resource is registered by using the Semian.register method.
|
31
|
+
#
|
32
|
+
# ==== Examples
|
33
|
+
#
|
34
|
+
# ===== Registering a resource
|
35
|
+
#
|
36
|
+
# Semian.register :mysql_shard0, tickets: 10, timeout: 0.5
|
37
|
+
#
|
38
|
+
# This registers a new resource called <code>:mysql_shard0</code> that has 10 tickets andd a default timeout of 500 milliseconds.
|
39
|
+
#
|
40
|
+
# ===== Using a resource
|
41
|
+
#
|
42
|
+
# Semian[:mysql_shard0].acquire do
|
43
|
+
# # Perform a MySQL query here
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# This acquires a ticket for the <code>:mysql_shard0</code> resource. If we use the example above, the ticket count would
|
47
|
+
# be lowered to 9 when block is executed, then raised to 10 when the block completes.
|
48
|
+
#
|
49
|
+
# ===== Overriding the default timeout
|
50
|
+
#
|
51
|
+
# Semian[:mysql_shard0].acquire(timeout: 1) do
|
52
|
+
# # Perform a MySQL query here
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# This is the same as the previous example, but overrides the timeout from the default value of 500 milliseconds to 1 second.
|
56
|
+
#
|
3
57
|
class Semian
|
4
58
|
class << self
|
5
|
-
|
6
|
-
|
59
|
+
# Registers a resource.
|
60
|
+
#
|
61
|
+
# +name+: Name of the resource - this can be either a string or symbol.
|
62
|
+
#
|
63
|
+
# +tickets+: Number of tickets. If this value is 0, the ticket count will not be set,
|
64
|
+
# but the resource must have been previously registered otherwise an error will be raised.
|
65
|
+
#
|
66
|
+
# +permissions+: Octal permissions of the resource.
|
67
|
+
#
|
68
|
+
# +timeout+: Default timeout in seconds.
|
69
|
+
#
|
70
|
+
# Returns the registered resource.
|
71
|
+
def register(name, tickets: 0, permissions: 0600, timeout: 1)
|
72
|
+
resource = Resource.new(name, tickets, permissions, timeout)
|
7
73
|
resources[name] = resource
|
8
74
|
end
|
9
75
|
|
76
|
+
# Retrieves a resource by name.
|
10
77
|
def [](name)
|
11
78
|
resources[name]
|
12
79
|
end
|
13
80
|
|
81
|
+
# Retrieves a hash of all registered resources.
|
14
82
|
def resources
|
15
83
|
@resources ||= {}
|
16
84
|
end
|
data/test/test_semian.rb
CHANGED
@@ -15,6 +15,16 @@ class TestSemian < Test::Unit::TestCase
|
|
15
15
|
assert_raises ArgumentError do
|
16
16
|
Semian.register :testing, tickets: -1
|
17
17
|
end
|
18
|
+
assert_raises ArgumentError do
|
19
|
+
Semian.register :testing, tickets: 1_000_000
|
20
|
+
end
|
21
|
+
assert_raises TypeError do
|
22
|
+
Semian.register :testing, permissions: "test"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_max_tickets
|
27
|
+
assert Semian::MAX_TICKETS > 0
|
18
28
|
end
|
19
29
|
|
20
30
|
def test_register
|
@@ -177,4 +187,113 @@ class TestSemian < Test::Unit::TestCase
|
|
177
187
|
end
|
178
188
|
end
|
179
189
|
|
190
|
+
def test_permissions
|
191
|
+
Semian.register :testing, permissions: 0600, tickets: 1
|
192
|
+
semid = Semian[:testing].semid
|
193
|
+
`ipcs -s `.lines.each do |line|
|
194
|
+
if /\s#{semid}\s/.match(line)
|
195
|
+
assert_equal '600', line.split[3]
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
Semian.register :testing, permissions: 0660, tickets: 1
|
200
|
+
semid = Semian[:testing].semid
|
201
|
+
`ipcs -s `.lines.each do |line|
|
202
|
+
if /\s#{semid}\s/.match(line)
|
203
|
+
assert_equal '660', line.split[3]
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def test_resize_tickets_increase
|
209
|
+
Semian.register :testing, tickets: 1
|
210
|
+
|
211
|
+
acquired = false
|
212
|
+
m = Monitor.new
|
213
|
+
cond = m.new_cond
|
214
|
+
|
215
|
+
t = Thread.start do
|
216
|
+
m.synchronize do
|
217
|
+
cond.wait_until { acquired }
|
218
|
+
|
219
|
+
Semian.register :testing, tickets: 5
|
220
|
+
assert_equal 4, Semian[:testing].count
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
assert_equal 1, Semian[:testing].count
|
225
|
+
|
226
|
+
Semian[:testing].acquire do
|
227
|
+
acquired = true
|
228
|
+
m.synchronize { cond.signal }
|
229
|
+
sleep 0.2
|
230
|
+
end
|
231
|
+
|
232
|
+
t.join
|
233
|
+
|
234
|
+
assert_equal 5, Semian[:testing].count
|
235
|
+
end
|
236
|
+
|
237
|
+
def test_resize_tickets_decrease
|
238
|
+
Semian.register :testing, tickets: 5
|
239
|
+
|
240
|
+
acquired = false
|
241
|
+
m = Monitor.new
|
242
|
+
cond = m.new_cond
|
243
|
+
|
244
|
+
t = Thread.start do
|
245
|
+
m.synchronize do
|
246
|
+
cond.wait_until { acquired }
|
247
|
+
|
248
|
+
Semian.register :testing, tickets: 1
|
249
|
+
assert_equal 0, Semian[:testing].count
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
assert_equal 5, Semian[:testing].count
|
254
|
+
|
255
|
+
Semian[:testing].acquire do
|
256
|
+
acquired = true
|
257
|
+
m.synchronize { cond.signal }
|
258
|
+
sleep 0.2
|
259
|
+
end
|
260
|
+
|
261
|
+
t.join
|
262
|
+
|
263
|
+
assert_equal 1, Semian[:testing].count
|
264
|
+
end
|
265
|
+
|
266
|
+
def test_multiple_register_with_fork
|
267
|
+
f = Tempfile.new('semian_test')
|
268
|
+
|
269
|
+
begin
|
270
|
+
f.flock(File::LOCK_EX)
|
271
|
+
|
272
|
+
children = []
|
273
|
+
5.times do
|
274
|
+
children << fork do
|
275
|
+
acquired = false
|
276
|
+
|
277
|
+
f.flock(File::LOCK_SH)
|
278
|
+
Semian.register(:testing, tickets: 5).acquire do |resource|
|
279
|
+
assert resource.count < 5
|
280
|
+
acquired = true
|
281
|
+
end
|
282
|
+
assert acquired
|
283
|
+
end
|
284
|
+
end
|
285
|
+
children.compact!
|
286
|
+
|
287
|
+
f.flock(File::LOCK_UN)
|
288
|
+
|
289
|
+
while children.any? do
|
290
|
+
children.delete(Process.wait)
|
291
|
+
end
|
292
|
+
|
293
|
+
assert_equal 5, Semian.register(:testing).count
|
294
|
+
ensure
|
295
|
+
f.close!
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
180
299
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: semian
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Scott Francis
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-10-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake-compiler
|