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 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