semian 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 569ed1de876236ee7b8aee71e9325fd609f6a65c
4
+ data.tar.gz: 1308dfeea5b5f554d97397a5eb79bfca484d6854
5
+ SHA512:
6
+ metadata.gz: 1e4e89901bc8654a0003e2a097984520508cf4143735d2d8dd35c0c9112ad292d0ca044f7471e54947a2ec974d07322719174d15226e7ce147140c63cac597c7
7
+ data.tar.gz: ecf5a1656a9bc1f51111eb9274ebb73844e40161e67894f1abe57dfbd59364e2007d7b5c5b6c5de652b6063d597741f372895b6251fef95316d790c4560022ca
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ /.bundle/
2
+ /lib/semian/*.so
3
+ /lib/semian/*.bundle
4
+ /tmp/*
5
+ *.gem
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.1
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,18 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ semian (0.0.1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ rake (10.3.2)
10
+ rake-compiler (0.9.3)
11
+ rake
12
+
13
+ PLATFORMS
14
+ ruby
15
+
16
+ DEPENDENCIES
17
+ rake-compiler (~> 0.9)
18
+ semian!
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Scott Francis <scott.francis@shopify.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ ## Semian
2
+
3
+ [![Build Status](https://travis-ci.org/csfrancis/semian.svg?branch=master)](https://travis-ci.org/csfrancis/semian)
4
+
5
+ Inspired by the bulkhead resource isolation pattern used in [Hystrix](https://github.com/Netflix/Hystrix/wiki/How-it-Works#Isolation), Semian aims to provide a Ruby API that can be used to control access to external resources.
6
+
7
+ This can be used with a forking Ruby application server like Unicorn to prevent app server starvation when a resource is slow or not responding.
8
+
9
+ ### Usage
10
+
11
+ In a master process, register a resource with a specified number of tickets (number of concurrent clients):
12
+ ```ruby
13
+ require 'semian'
14
+
15
+ Semian.register(:mysql_master, tickets: 3, timeout: 0.5)
16
+ ```
17
+
18
+ Then in your child processes, you can use the resource:
19
+ ```ruby
20
+ Semian[:mysql_master].acquire do
21
+ # Query mysql and do things
22
+ end
23
+ ```
24
+
25
+ If you have a process that does not fork, you can still use the same namespace to control access to a shared resource:
26
+ ```ruby
27
+ Semian.register(:mysql_master, timeout: 0.5)
28
+ Semian[:mysql_master].acquire do
29
+ # Query mysql and do things
30
+ end
31
+ ```
data/Rakefile ADDED
@@ -0,0 +1,32 @@
1
+ task :default => :test
2
+
3
+ # ==========================================================
4
+ # Packaging
5
+ # ==========================================================
6
+
7
+ GEMSPEC = eval(File.read('semian.gemspec'))
8
+
9
+ require 'rubygems/package_task'
10
+ Gem::PackageTask.new(GEMSPEC) do |pkg|
11
+ end
12
+
13
+ # ==========================================================
14
+ # Ruby Extension
15
+ # ==========================================================
16
+
17
+ require 'rake/extensiontask'
18
+ Rake::ExtensionTask.new('semian', GEMSPEC) do |ext|
19
+ ext.ext_dir = 'ext/semian'
20
+ ext.lib_dir = 'lib/semian'
21
+ end
22
+ task :build => :compile
23
+
24
+ # ==========================================================
25
+ # Testing
26
+ # ==========================================================
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new 'test' do |t|
30
+ t.test_files = FileList['test/test_*.rb']
31
+ end
32
+ task :test => :build
@@ -0,0 +1,15 @@
1
+ require 'mkmf'
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')
7
+
8
+ have_header 'sys/ipc.h'
9
+ have_header 'sys/sem.h'
10
+ have_header 'sys/types.h'
11
+
12
+ have_func 'rb_thread_blocking_region'
13
+ have_func 'rb_thread_call_without_gvl'
14
+
15
+ create_makefile('semian/semian')
@@ -0,0 +1,248 @@
1
+ #include <sys/types.h>
2
+ #include <sys/ipc.h>
3
+ #include <sys/sem.h>
4
+ #include <errno.h>
5
+ #include <string.h>
6
+
7
+ #include <ruby.h>
8
+ #include <ruby/util.h>
9
+ #include <ruby/io.h>
10
+
11
+ #include <openssl/sha.h>
12
+
13
+ #include <stdio.h>
14
+
15
+ #if defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL) && defined(HAVE_RUBY_THREAD_H)
16
+ // 2.0
17
+ #include <ruby/thread.h>
18
+ #define WITHOUT_GVL(fn,a,ubf,b) rb_thread_call_without_gvl((fn),(a),(ubf),(b))
19
+ #elif defined(HAVE_RB_THREAD_BLOCKING_REGION)
20
+ // 1.9
21
+ typedef VALUE (*my_blocking_fn_t)(void*);
22
+ #define WITHOUT_GVL(fn,a,ubf,b) rb_thread_blocking_region((my_blocking_fn_t)(fn),(a),(ubf),(b))
23
+ #endif
24
+
25
+ static ID id_timeout;
26
+ static VALUE eTimeout;
27
+
28
+ typedef struct {
29
+ int sem_id;
30
+ struct timespec timeout;
31
+ int error;
32
+ char *name;
33
+ } semian_resource_t;
34
+
35
+ static key_t
36
+ generate_key(const char *name)
37
+ {
38
+ char digest[SHA_DIGEST_LENGTH];
39
+ SHA1(name, strlen(name), digest);
40
+ /* TODO: compile-time assertion that sizeof(key_t) > SHA_DIGEST_LENGTH */
41
+ return *((key_t *) digest);
42
+ }
43
+
44
+ static void
45
+ ms_to_timespec(long ms, struct timespec *ts)
46
+ {
47
+ ts->tv_sec = ms / 1000;
48
+ ts->tv_nsec = (ms % 1000) * 1000000;
49
+ }
50
+
51
+ static void
52
+ semian_resource_mark(void *ptr)
53
+ {
54
+ /* noop */
55
+ }
56
+
57
+ static void
58
+ semian_resource_free(void *ptr)
59
+ {
60
+ semian_resource_t *res = (semian_resource_t *) ptr;
61
+ if (res->name) {
62
+ free(res->name);
63
+ res->name = NULL;
64
+ }
65
+ xfree(res);
66
+ }
67
+
68
+ static size_t
69
+ semian_resource_memsize(const void *ptr)
70
+ {
71
+ return sizeof(semian_resource_t);
72
+ }
73
+
74
+ static const rb_data_type_t
75
+ semian_resource_type = {
76
+ "semian_resource",
77
+ {
78
+ semian_resource_mark,
79
+ semian_resource_free,
80
+ semian_resource_memsize
81
+ },
82
+ NULL, NULL, RUBY_TYPED_FREE_IMMEDIATELY
83
+ };
84
+
85
+ static VALUE
86
+ semian_resource_alloc(VALUE klass)
87
+ {
88
+ semian_resource_t *res;
89
+ VALUE obj = TypedData_Make_Struct(klass, semian_resource_t, &semian_resource_type, res);
90
+ return obj;
91
+ }
92
+
93
+ static VALUE
94
+ semian_resource_initialize(VALUE self, VALUE id, VALUE tickets, VALUE default_timeout)
95
+ {
96
+ key_t key;
97
+ int flags = S_IRUSR | S_IWUSR;
98
+ semian_resource_t *res = NULL;
99
+ const char *id_str = NULL;
100
+
101
+ if (TYPE(id) != T_SYMBOL && TYPE(id) != T_STRING) {
102
+ rb_raise(rb_eTypeError, "id must be a symbol or string");
103
+ }
104
+ Check_Type(tickets, T_FIXNUM);
105
+ if (TYPE(default_timeout) != T_FIXNUM && TYPE(default_timeout) != T_FLOAT) {
106
+ rb_raise(rb_eTypeError, "expected numeric type for default_timeout");
107
+ }
108
+ if (FIX2LONG(tickets) < 0) {
109
+ rb_raise(rb_eArgError, "ticket count must be a non-negative value");
110
+ }
111
+ if (NUM2DBL(default_timeout) < 0) {
112
+ rb_raise(rb_eArgError, "default timeout must be non-negative value");
113
+ }
114
+
115
+ if (TYPE(id) == T_SYMBOL) {
116
+ id_str = rb_id2name(rb_to_id(id));
117
+ } else if (TYPE(id) == T_STRING) {
118
+ id_str = RSTRING_PTR(id);
119
+ }
120
+ TypedData_Get_Struct(self, semian_resource_t, &semian_resource_type, res);
121
+ key = generate_key(id_str);
122
+ ms_to_timespec(NUM2DBL(default_timeout) * 1000, &res->timeout);
123
+ res->name = strdup(id_str);
124
+
125
+ if (FIX2LONG(tickets) != 0) {
126
+ flags |= IPC_CREAT;
127
+ }
128
+
129
+ res->sem_id = semget(key, 1, flags);
130
+ if (res->sem_id == -1) {
131
+ rb_sys_fail("semget");
132
+ }
133
+
134
+ if (FIX2LONG(tickets) != 0
135
+ && semctl(res->sem_id, 0, SETVAL, FIX2LONG(tickets)) == -1) {
136
+ rb_sys_fail("semctl");
137
+ }
138
+
139
+ return self;
140
+ }
141
+
142
+ static VALUE
143
+ cleanup_semian_resource_acquire(VALUE self)
144
+ {
145
+ semian_resource_t *res = NULL;
146
+ TypedData_Get_Struct(self, semian_resource_t, &semian_resource_type, res);
147
+ struct sembuf buf = { 0, 1, 0 };
148
+ if (semop(res->sem_id, &buf, 1) == -1) {
149
+ res->error = errno;
150
+ }
151
+ return Qnil;
152
+ }
153
+
154
+ static void *
155
+ acquire_sempahore_without_gvl(void *p)
156
+ {
157
+ semian_resource_t *res = (semian_resource_t *) p;
158
+ struct sembuf buf = { 0, -1, SEM_UNDO };
159
+ res->error = 0;
160
+ if (semtimedop(res->sem_id, &buf, 1, &res->timeout) == -1) {
161
+ res->error = errno;
162
+ }
163
+ return NULL;
164
+ }
165
+
166
+ static VALUE
167
+ semian_resource_acquire(int argc, VALUE *argv, VALUE self)
168
+ {
169
+ semian_resource_t *self_res = NULL;
170
+ semian_resource_t res = { 0 };
171
+
172
+ if (!rb_block_given_p()) {
173
+ rb_raise(rb_eArgError, "acquire requires a block");
174
+ }
175
+
176
+ TypedData_Get_Struct(self, semian_resource_t, &semian_resource_type, self_res);
177
+ res = *self_res;
178
+
179
+ /* allow the default timeout to be overridden by a "timeout" param */
180
+ if (argc == 1 && TYPE(argv[0]) == T_HASH) {
181
+ VALUE timeout = rb_hash_aref(argv[0], ID2SYM(id_timeout));
182
+ if (TYPE(timeout) != T_NIL) {
183
+ if (TYPE(timeout) != T_FLOAT && TYPE(timeout) != T_FIXNUM) {
184
+ rb_raise(rb_eArgError, "timeout parameter must be numeric");
185
+ }
186
+ ms_to_timespec(NUM2DBL(timeout) * 1000, &res.timeout);
187
+ }
188
+ } else if (argc > 0) {
189
+ rb_raise(rb_eArgError, "invalid arguments");
190
+ }
191
+
192
+ /* release the GVL to acquire the semaphore */
193
+ WITHOUT_GVL(acquire_sempahore_without_gvl, &res, RUBY_UBF_IO, NULL);
194
+ if (res.error != 0) {
195
+ if (res.error == EAGAIN) {
196
+ rb_raise(eTimeout, "timed out waiting for resource '%s'", res.name);
197
+ } else {
198
+ rb_raise(rb_eRuntimeError, "semop() error: %s (%d)", strerror(res.error), res.error);
199
+ }
200
+ }
201
+
202
+ return rb_ensure(rb_yield, self, cleanup_semian_resource_acquire, self);
203
+ }
204
+
205
+ static VALUE
206
+ semian_resource_destroy(VALUE self)
207
+ {
208
+ semian_resource_t *res = NULL;
209
+
210
+ TypedData_Get_Struct(self, semian_resource_t, &semian_resource_type, res);
211
+ if (semctl(res->sem_id, 0, IPC_RMID) == -1) {
212
+ rb_sys_fail("semctl");
213
+ }
214
+
215
+ return Qtrue;
216
+ }
217
+
218
+ static VALUE
219
+ semian_resource_count(VALUE self)
220
+ {
221
+ int ret;
222
+ semian_resource_t *res = NULL;
223
+
224
+ TypedData_Get_Struct(self, semian_resource_t, &semian_resource_type, res);
225
+ ret = semctl(res->sem_id, 0, GETVAL);
226
+ if (ret == -1) {
227
+ rb_raise(rb_eRuntimeError, "semctl() error: %s (%d)", strerror(errno), errno);
228
+ }
229
+
230
+ return LONG2FIX(ret);
231
+ }
232
+
233
+ void Init_semian()
234
+ {
235
+ VALUE cSemian, cResource;
236
+
237
+ cSemian = rb_define_class("Semian", rb_cObject);
238
+ cResource = rb_define_class("Resource", cSemian);
239
+ eTimeout = rb_define_class_under(cSemian, "Timeout", rb_eStandardError);
240
+
241
+ rb_define_alloc_func(cResource, semian_resource_alloc);
242
+ rb_define_method(cResource, "initialize", semian_resource_initialize, 3);
243
+ rb_define_method(cResource, "acquire", semian_resource_acquire, -1);
244
+ rb_define_method(cResource, "count", semian_resource_count, 0);
245
+ rb_define_method(cResource, "destroy", semian_resource_destroy, 0);
246
+
247
+ id_timeout = rb_intern("timeout");
248
+ }
data/lib/semian.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'semian/semian'
2
+
3
+ class Semian
4
+ class << self
5
+ def register(name, tickets: 0, timeout: 1)
6
+ resource = Resource.new(name, tickets, timeout)
7
+ resources[name] = resource
8
+ end
9
+
10
+ def [](name)
11
+ resources[name]
12
+ end
13
+
14
+ def resources
15
+ @resources ||= {}
16
+ end
17
+ end
18
+ end
data/semian.gemspec ADDED
@@ -0,0 +1,17 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'semian'
3
+ s.version = '0.0.1'
4
+ s.summary = 'SysV semaphore based library for shared resource control'
5
+ s.description = <<-DOC
6
+ A Ruby C extention that is used to control access to shared resources
7
+ across process boundaries.
8
+ DOC
9
+ s.homepage = 'https://github.com/csfrancis/semian'
10
+ s.authors = 'Scott Francis'
11
+ s.email = 'scott.francis@shopify.com'
12
+ s.license = 'MIT'
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.extensions = ['ext/semian/extconf.rb']
16
+ s.add_development_dependency 'rake-compiler', '~> 0.9'
17
+ end
@@ -0,0 +1,166 @@
1
+ require 'test/unit'
2
+ require 'semian'
3
+ require 'tempfile'
4
+ require 'fileutils'
5
+
6
+ class TestSemian < Test::Unit::TestCase
7
+ def setup
8
+ Semian[:testing].destroy rescue RuntimeError
9
+ end
10
+
11
+ def test_register_invalid_args
12
+ assert_raises TypeError do
13
+ Semian.register 123
14
+ end
15
+ assert_raises ArgumentError do
16
+ Semian.register :testing, tickets: -1
17
+ end
18
+ end
19
+
20
+ def test_register
21
+ Semian.register :testing, tickets: 2
22
+ end
23
+
24
+ def test_register_with_no_tickets_raises
25
+ assert_raises Errno::ENOENT do
26
+ Semian.register :testing
27
+ end
28
+ end
29
+
30
+ def test_acquire
31
+ acquired = false
32
+ Semian.register :testing, tickets: 1
33
+ Semian[:testing].acquire { acquired = true }
34
+ assert acquired
35
+ end
36
+
37
+ def test_acquire_return_val
38
+ Semian.register :testing, tickets: 1
39
+ val = Semian[:testing].acquire { 1234 }
40
+ assert_equal 1234, val
41
+ end
42
+
43
+ def test_acquire_timeout
44
+ Semian.register :testing, tickets: 1, timeout: 0.05
45
+
46
+ acquired = false
47
+ m = Monitor.new
48
+ cond = m.new_cond
49
+
50
+ t = Thread.start do
51
+ m.synchronize do
52
+ cond.wait_until { acquired }
53
+ assert_raises Semian::Timeout do
54
+ Semian[:testing].acquire { refute true }
55
+ end
56
+ end
57
+ end
58
+
59
+ Semian[:testing].acquire do
60
+ acquired = true
61
+ m.synchronize { cond.signal }
62
+ sleep 0.2
63
+ end
64
+
65
+ t.join
66
+
67
+ assert acquired
68
+ end
69
+
70
+ def test_acquire_timeout_override
71
+ Semian.register :testing, tickets: 1, timeout: 0.01
72
+
73
+ acquired = false
74
+ thread_acquired = false
75
+ m = Monitor.new
76
+ cond = m.new_cond
77
+
78
+ t = Thread.start do
79
+ m.synchronize do
80
+ cond.wait_until { acquired }
81
+ Semian[:testing].acquire(timeout: 1) { thread_acquired = true }
82
+ end
83
+ end
84
+
85
+ Semian[:testing].acquire do
86
+ acquired = true
87
+ m.synchronize { cond.signal }
88
+ sleep 0.2
89
+ end
90
+
91
+ t.join
92
+
93
+ assert acquired
94
+ assert thread_acquired
95
+ end
96
+
97
+ def test_acquire_with_fork
98
+ Semian.register :testing, tickets: 2, timeout: 0.5
99
+
100
+ Semian[:testing].acquire do
101
+ pid = fork do
102
+ Semian.register :testing, timeout: 0.5
103
+ Semian[:testing].acquire do
104
+ assert_raises Semian::Timeout do
105
+ Semian[:testing].acquire { }
106
+ end
107
+ end
108
+ end
109
+
110
+ Process.wait
111
+ end
112
+ end
113
+
114
+ def test_acquire_releases_on_kill
115
+ begin
116
+ Semian.register :testing, tickets: 1, timeout: 0.1
117
+ acquired = false
118
+
119
+ # Ghetto process synchronization
120
+ file = Tempfile.new('semian')
121
+ path = file.path
122
+ file.close!
123
+
124
+ pid = fork do
125
+ Semian[:testing].acquire do
126
+ FileUtils.touch(path)
127
+ sleep 1000
128
+ end
129
+ end
130
+
131
+ sleep 0.1 until File.exists?(path)
132
+ assert_raises Semian::Timeout do
133
+ Semian[:testing].acquire {}
134
+ end
135
+
136
+ Process.kill("KILL", pid)
137
+ Semian[:testing].acquire { acquired = true }
138
+ assert acquired
139
+
140
+ Process.wait
141
+ ensure
142
+ FileUtils.rm_f(path) if path
143
+ end
144
+ end
145
+
146
+ def test_count
147
+ Semian.register :testing, tickets: 2
148
+ acquired = false
149
+
150
+ Semian[:testing].acquire do
151
+ acquired = true
152
+ assert_equal 1, Semian[:testing].count
153
+ end
154
+
155
+ assert acquired
156
+ end
157
+
158
+ def test_destroy
159
+ Semian.register :testing, tickets: 1
160
+ Semian[:testing].destroy
161
+ assert_raises RuntimeError do
162
+ Semian[:testing].acquire { }
163
+ end
164
+ end
165
+
166
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: semian
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Scott Francis
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-09-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake-compiler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.9'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.9'
27
+ description: |2
28
+ A Ruby C extention that is used to control access to shared resources
29
+ across process boundaries.
30
+ email: scott.francis@shopify.com
31
+ executables: []
32
+ extensions:
33
+ - ext/semian/extconf.rb
34
+ extra_rdoc_files: []
35
+ files:
36
+ - ".gitignore"
37
+ - ".travis.yml"
38
+ - Gemfile
39
+ - Gemfile.lock
40
+ - LICENSE.md
41
+ - README.md
42
+ - Rakefile
43
+ - ext/semian/extconf.rb
44
+ - ext/semian/semian.c
45
+ - lib/semian.rb
46
+ - semian.gemspec
47
+ - test/test_semian.rb
48
+ homepage: https://github.com/csfrancis/semian
49
+ licenses:
50
+ - MIT
51
+ metadata: {}
52
+ post_install_message:
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubyforge_project:
68
+ rubygems_version: 2.2.2
69
+ signing_key:
70
+ specification_version: 4
71
+ summary: SysV semaphore based library for shared resource control
72
+ test_files: []