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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b36c09e6dccc4e072f3aac227c2cdb9d34310342
4
- data.tar.gz: 8aae6e974ae678aa7f9cd97ec33addbb893c354b
3
+ metadata.gz: 04219b7eea123cad23d49905ec52c376484013fb
4
+ data.tar.gz: 4922f4c4f148425ebb7d1ced40dc8bf6fc3454ec
5
5
  SHA512:
6
- metadata.gz: 1f13efc113b4bd1cf589ce12f8a2528df8e9d68e671e6ced785eac20e1deb97e523bc9057b01024cee7c5e41fc7045755846471108bf2b1d971e13d86e19d01f
7
- data.tar.gz: 0280c4d59e8af8307c6f46c5bbb91c2f631b27ae306dd8c7009d583c6470b7cb74eee74b60e96b7db68d6ae53c8a4dc92dbf310fc806acffe01b395565c2a85d
6
+ metadata.gz: 896373f2c1401134888a8ac3d503663bcbec528f7d304173196310ec7a585f3e5c01faa0c92ce199ba935b8324e4fa57a1f177f77503bfcd59219e64f576952e
7
+ data.tar.gz: ea74197c538ba50e31c01a518570f3bb94f60d5f28be77554616dc9c2595cbf4be24e280cff612d66ef0c970737f025971f93316c2c6a9a3d9a83e1472698e20
data/.gitignore CHANGED
@@ -3,3 +3,4 @@
3
3
  /lib/semian/*.bundle
4
4
  /tmp/*
5
5
  *.gem
6
+ /html/
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- semian (0.0.2)
4
+ semian (0.0.4)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
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
@@ -1,9 +1,7 @@
1
1
  require 'mkmf'
2
2
 
3
- $CFLAGS = "-O3"
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
- char digest[SHA_DIGEST_LENGTH];
39
- SHA1(name, strlen(name), digest);
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 *((key_t *) digest);
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 flags = S_IRUSR | S_IWUSR;
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
- if (FIX2LONG(tickets) != 0) {
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
- if (FIX2LONG(tickets) != 0
141
- && semctl(res->sem_id, 0, SETVAL, FIX2LONG(tickets)) == -1) {
142
- raise_semian_syscall_error("semctl()", errno);
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
- struct sembuf buf = { 0, 1, SEM_UNDO };
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 (semtimedop(res->sem_id, &buf, 1, &res->timeout) == -1) {
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
- cResource = rb_define_class("Resource", cSemian);
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, 3);
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
  }
@@ -1,3 +1,3 @@
1
1
  class Semian
2
- VERSION = '0.0.3'
2
+ VERSION = '0.0.4'
3
3
  end
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
- def register(name, tickets: 0, timeout: 1)
6
- resource = Resource.new(name, tickets, timeout)
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.3
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-09-29 00:00:00.000000000 Z
11
+ date: 2014-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake-compiler