semian 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|