semian 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +29 -12
- data/ext/semian/resource.c +4 -4
- data/ext/semian/sysv_semaphores.c +16 -0
- data/ext/semian/sysv_semaphores.h +5 -7
- data/lib/semian/circuit_breaker.rb +13 -20
- data/lib/semian/grpc.rb +1 -1
- data/lib/semian/lru_hash.rb +10 -7
- data/lib/semian/platform.rb +1 -1
- data/lib/semian/protected_resource.rb +9 -3
- data/lib/semian/unprotected_resource.rb +1 -1
- data/lib/semian/version.rb +1 -1
- data/lib/semian.rb +27 -12
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '0461852ea5a5ffbacaf9a20062213609066e3ef8ade9a93a980d2af73a0feb39'
|
4
|
+
data.tar.gz: 8e21023912cd7c71cc74c0eb6a384771018f5fa3f53c212a62c23446801573e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0bd910ad2f7891d4dc2660135adea394db252afa7f53f8c707a55fa569cbf481514af485fe9056d5959e98faffd8bd20990ac9f7b6abe0b0f453ca29d3ad3e9f
|
7
|
+
data.tar.gz: a4149e3b4021fe47be636e28bbd5ece3eb4246d65bbc95b07cc50e1b45a328980e47c32d15e81a52e2516fe01b2622fedf31d5aac8bcedbee7e3bdb73a1a0c8c
|
data/README.md
CHANGED
@@ -73,6 +73,7 @@ version is the version of the public gem with the same name:
|
|
73
73
|
* [`semian/mysql2`][mysql-semian-adapter] (~> 0.3.16)
|
74
74
|
* [`semian/redis`][redis-semian-adapter] (~> 3.2.1)
|
75
75
|
* [`semian/net_http`][nethttp-semian-adapter]
|
76
|
+
* [`semian-postgres`][postgres-semian-adapter]
|
76
77
|
|
77
78
|
### Creating Adapters
|
78
79
|
|
@@ -110,7 +111,7 @@ There are some global configuration options that can be set for Semian:
|
|
110
111
|
# Note: Setting this to 0 enables aggressive garbage collection.
|
111
112
|
Semian.maximum_lru_size = 0
|
112
113
|
|
113
|
-
# Minimum time a resource should be resident in the LRU cache (default: 300s)
|
114
|
+
# Minimum time in seconds a resource should be resident in the LRU cache (default: 300s)
|
114
115
|
Semian.minimum_lru_time = 60
|
115
116
|
```
|
116
117
|
|
@@ -418,9 +419,11 @@ response time. This is the problem Semian solves by failing fast.
|
|
418
419
|
|
419
420
|
## How does Semian work?
|
420
421
|
|
421
|
-
Semian consists of two parts: **Circuit
|
422
|
-
Semian, and especially how to configure it,
|
423
|
-
and their implementation.
|
422
|
+
Semian consists of two parts: **Circuit Breaker** and **Bulkheading**.
|
423
|
+
To understand Semian, and especially how to configure it,
|
424
|
+
we must understand these patterns and their implementation.
|
425
|
+
|
426
|
+
Disable Semian via environment variable `SEMIAN_DISABLED=1`.
|
424
427
|
|
425
428
|
### Circuit Breaker
|
426
429
|
|
@@ -451,18 +454,28 @@ all workers on a server.
|
|
451
454
|
|
452
455
|
There are four configuration parameters for circuit breakers in Semian:
|
453
456
|
|
454
|
-
* **
|
455
|
-
|
456
|
-
|
457
|
+
* **circuit_breaker**. Enable or Disable Circuit Breaker. Defaults to `true` if not set.
|
458
|
+
* **error_threshold**. The amount of errors a worker encounters within `error_threshold_timeout`
|
459
|
+
amount of time before opening the circuit,
|
460
|
+
that is to start rejecting requests instantly.
|
461
|
+
* **error_threshold_timeout**. The amount of time in seconds that `error_threshold`
|
462
|
+
errors must occur to open the circuit.
|
463
|
+
Defaults to `error_timeout` seconds if not set.
|
457
464
|
* **error_timeout**. The amount of time in seconds until trying to query the resource
|
458
465
|
again.
|
459
|
-
* **error_threshold_timeout_enabled**. If set to false it will disable
|
460
|
-
the
|
466
|
+
* **error_threshold_timeout_enabled**. If set to false it will disable
|
467
|
+
the time window for evicting old exceptions. `error_timeout` is still used and
|
468
|
+
will reset the circuit. Defaults to `true` if not set.
|
461
469
|
* **success_threshold**. The amount of successes on the circuit until closing it
|
462
470
|
again, that is to start accepting all requests to the circuit.
|
463
|
-
* **half_open_resource_timeout**. Timeout for the resource in seconds when
|
471
|
+
* **half_open_resource_timeout**. Timeout for the resource in seconds when
|
472
|
+
the circuit is half-open (supported for MySQL, Net::HTTP and Redis).
|
473
|
+
|
474
|
+
It is possible to disable Circuit Breaker with environment variable
|
475
|
+
`SEMIAN_CIRCUIT_BREAKER_DISABLED=1`.
|
464
476
|
|
465
|
-
For more information about configuring these parameters, please read
|
477
|
+
For more information about configuring these parameters, please read
|
478
|
+
[this post](https://engineering.shopify.com/blogs/engineering/circuit-breaker-misconfigured).
|
466
479
|
|
467
480
|
### Bulkheading
|
468
481
|
|
@@ -515,11 +528,15 @@ still experimenting with ways to figure out optimal ticket numbers. Generally
|
|
515
528
|
something below half the number of workers on the server for endpoints that are
|
516
529
|
queried frequently has worked well for us.
|
517
530
|
|
531
|
+
* **bulkhead**. Enable or Disable Bulkhead. Defaults to `true` if not set.
|
518
532
|
* **tickets**. Number of workers that can concurrently access a resource.
|
519
533
|
* **timeout**. Time to wait in seconds to acquire a ticket if there are no tickets left.
|
520
534
|
We recommend this to be `0` unless you have very few workers running (i.e.
|
521
535
|
less than ~5).
|
522
536
|
|
537
|
+
It is possible to disable Bulkhead with environment variable
|
538
|
+
`SEMIAN_BULKHEAD_DISABLED=1`.
|
539
|
+
|
523
540
|
Note that there are system-wide limitations on how many tickets can be allocated
|
524
541
|
on a system. `cat /proc/sys/kernel/sem` will tell you.
|
525
542
|
|
@@ -549,7 +566,6 @@ ipcs -si $(ipcs -s | grep 0x48af51ea | awk '{print $2}')
|
|
549
566
|
Which should output something like:
|
550
567
|
|
551
568
|
```
|
552
|
-
|
553
569
|
Semaphore Array semid=5570729
|
554
570
|
uid=8192 gid=8192 cuid=8192 cgid=8192
|
555
571
|
mode=0660, access_perms=0660
|
@@ -841,6 +857,7 @@ $ bundle install
|
|
841
857
|
[release-it]: https://pragprog.com/titles/mnee2/release-it-second-edition/
|
842
858
|
[shopify]: http://www.shopify.com/
|
843
859
|
[mysql-semian-adapter]: lib/semian/mysql2.rb
|
860
|
+
[postgres-semian-adapter]: https://github.com/mschoenlaub/semian-postgres
|
844
861
|
[redis-semian-adapter]: lib/semian/redis.rb
|
845
862
|
[semian-adapter]: lib/semian/adapter.rb
|
846
863
|
[nethttp-semian-adapter]: lib/semian/net_http.rb
|
data/ext/semian/resource.c
CHANGED
@@ -359,13 +359,13 @@ ms_to_timespec(long ms, struct timespec *ts)
|
|
359
359
|
ts->tv_nsec = (ms % 1000) * 1000000;
|
360
360
|
}
|
361
361
|
|
362
|
-
static
|
362
|
+
static void
|
363
363
|
semian_resource_mark(void *ptr)
|
364
364
|
{
|
365
365
|
/* noop */
|
366
366
|
}
|
367
367
|
|
368
|
-
static
|
368
|
+
static void
|
369
369
|
semian_resource_free(void *ptr)
|
370
370
|
{
|
371
371
|
semian_resource_t *res = (semian_resource_t *) ptr;
|
@@ -376,7 +376,7 @@ semian_resource_free(void *ptr)
|
|
376
376
|
xfree(res);
|
377
377
|
}
|
378
378
|
|
379
|
-
static
|
379
|
+
static size_t
|
380
380
|
semian_resource_memsize(const void *ptr)
|
381
381
|
{
|
382
382
|
return sizeof(semian_resource_t);
|
@@ -390,5 +390,5 @@ semian_resource_type = {
|
|
390
390
|
semian_resource_free,
|
391
391
|
semian_resource_memsize
|
392
392
|
},
|
393
|
-
NULL, NULL, RUBY_TYPED_FREE_IMMEDIATELY
|
393
|
+
NULL, NULL, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
|
394
394
|
};
|
@@ -269,3 +269,19 @@ diff_timespec_ms(struct timespec *end, struct timespec *begin)
|
|
269
269
|
long begin_ms = (begin->tv_sec * 1e3) + (begin->tv_nsec / 1e6);
|
270
270
|
return end_ms - begin_ms;
|
271
271
|
}
|
272
|
+
|
273
|
+
#ifdef DEBUG
|
274
|
+
VALUE
|
275
|
+
print_sem_vals_without_rescue(VALUE v_sem_id)
|
276
|
+
{
|
277
|
+
int sem_id = NUM2INT(v_sem_id);
|
278
|
+
printf("[pid=%d][semian] semaphore values lock: %d, tickets: %d configured: %d, registered workers: %d\n",
|
279
|
+
getpid(),
|
280
|
+
get_sem_val(sem_id, SI_SEM_LOCK),
|
281
|
+
get_sem_val(sem_id, SI_SEM_TICKETS),
|
282
|
+
get_sem_val(sem_id, SI_SEM_CONFIGURED_TICKETS),
|
283
|
+
get_sem_val(sem_id, SI_SEM_REGISTERED_WORKERS)
|
284
|
+
);
|
285
|
+
return (VALUE)0;
|
286
|
+
}
|
287
|
+
#endif
|
@@ -110,16 +110,14 @@ void *
|
|
110
110
|
acquire_semaphore_without_gvl(void *p);
|
111
111
|
|
112
112
|
#ifdef DEBUG
|
113
|
+
VALUE
|
114
|
+
print_sem_vals_without_rescue(VALUE v_sem_id);
|
115
|
+
|
113
116
|
static inline void
|
114
117
|
print_sem_vals(int sem_id)
|
115
118
|
{
|
116
|
-
|
117
|
-
|
118
|
-
get_sem_val(sem_id, SI_SEM_LOCK),
|
119
|
-
get_sem_val(sem_id, SI_SEM_TICKETS),
|
120
|
-
get_sem_val(sem_id, SI_SEM_CONFIGURED_TICKETS),
|
121
|
-
get_sem_val(sem_id, SI_SEM_REGISTERED_WORKERS)
|
122
|
-
);
|
119
|
+
int state;
|
120
|
+
rb_protect(print_sem_vals_without_rescue, INT2NUM(sem_id), &state);
|
123
121
|
}
|
124
122
|
#endif
|
125
123
|
|
@@ -36,8 +36,6 @@ module Semian
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def acquire(resource = nil, &block)
|
39
|
-
return yield if disabled?
|
40
|
-
|
41
39
|
transition_to_half_open if transition_to_half_open?
|
42
40
|
|
43
41
|
raise OpenCircuitError unless request_allowed?
|
@@ -66,7 +64,7 @@ module Semian
|
|
66
64
|
|
67
65
|
def mark_failed(error)
|
68
66
|
push_error(error)
|
69
|
-
push_time
|
67
|
+
push_time
|
70
68
|
if closed?
|
71
69
|
transition_to_open if error_threshold_reached?
|
72
70
|
elsif half_open?
|
@@ -131,19 +129,20 @@ module Semian
|
|
131
129
|
last_error_time = @errors.last
|
132
130
|
return false unless last_error_time
|
133
131
|
|
134
|
-
|
132
|
+
last_error_time + @error_timeout < Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
135
133
|
end
|
136
134
|
|
137
135
|
def push_error(error)
|
138
136
|
@last_error = error
|
139
137
|
end
|
140
138
|
|
141
|
-
def push_time
|
139
|
+
def push_time
|
140
|
+
time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
142
141
|
if error_threshold_timeout_enabled
|
143
|
-
|
142
|
+
@errors.reject! { |err_time| err_time + @error_threshold_timeout < time }
|
144
143
|
end
|
145
144
|
|
146
|
-
|
145
|
+
@errors << time
|
147
146
|
end
|
148
147
|
|
149
148
|
def log_state_transition(new_state)
|
@@ -151,7 +150,8 @@ module Semian
|
|
151
150
|
|
152
151
|
str = "[#{self.class.name}] State transition from #{@state.value} to #{new_state}."
|
153
152
|
str += " success_count=#{@successes.value} error_count=#{@errors.size}"
|
154
|
-
str += " success_count_threshold=#{@success_count_threshold}
|
153
|
+
str += " success_count_threshold=#{@success_count_threshold}"
|
154
|
+
str += " error_count_threshold=#{@error_count_threshold}"
|
155
155
|
str += " error_timeout=#{@error_timeout} error_last_at=\"#{@errors.last}\""
|
156
156
|
str += " name=\"#{@name}\""
|
157
157
|
if new_state == :open && @last_error
|
@@ -165,21 +165,14 @@ module Semian
|
|
165
165
|
Semian.notify(:state_change, self, nil, nil, state: new_state)
|
166
166
|
end
|
167
167
|
|
168
|
-
def disabled?
|
169
|
-
ENV["SEMIAN_CIRCUIT_BREAKER_DISABLED"] || ENV["SEMIAN_DISABLED"]
|
170
|
-
end
|
171
|
-
|
172
168
|
def maybe_with_half_open_resource_timeout(resource, &block)
|
173
|
-
|
174
|
-
|
175
|
-
resource.with_resource_timeout(@half_open_resource_timeout) do
|
176
|
-
block.call
|
177
|
-
end
|
178
|
-
else
|
169
|
+
if half_open? && @half_open_resource_timeout && resource.respond_to?(:with_resource_timeout)
|
170
|
+
resource.with_resource_timeout(@half_open_resource_timeout) do
|
179
171
|
block.call
|
180
172
|
end
|
181
|
-
|
182
|
-
|
173
|
+
else
|
174
|
+
block.call
|
175
|
+
end
|
183
176
|
end
|
184
177
|
end
|
185
178
|
end
|
data/lib/semian/grpc.rb
CHANGED
@@ -117,7 +117,7 @@ module Semian
|
|
117
117
|
execute = operation.singleton_method(:execute)
|
118
118
|
operation.instance_variable_set(:@semian, self)
|
119
119
|
operation.define_singleton_method(:execute) do
|
120
|
-
@semian.send(:acquire_semian_resource,
|
120
|
+
@semian.send(:acquire_semian_resource, adapter: :grpc, scope: scope) { execute.call }
|
121
121
|
end
|
122
122
|
end
|
123
123
|
end
|
data/lib/semian/lru_hash.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "thread"
|
4
|
+
|
3
5
|
class LRUHash
|
4
6
|
# This LRU (Least Recently Used) hash will allow
|
5
7
|
# the cleaning of resources as time goes on.
|
@@ -41,7 +43,7 @@ class LRUHash
|
|
41
43
|
#
|
42
44
|
# Arguments:
|
43
45
|
# +max_size+ The maximum size of the table
|
44
|
-
# +min_time+ The minimum time a resource can live in the cache
|
46
|
+
# +min_time+ The minimum time in seconds a resource can live in the cache
|
45
47
|
#
|
46
48
|
# Note:
|
47
49
|
# The +min_time+ is a stronger guarantee than +max_size+. That is, if there are
|
@@ -57,9 +59,9 @@ class LRUHash
|
|
57
59
|
@table = {}
|
58
60
|
@lock =
|
59
61
|
if Semian.thread_safe?
|
60
|
-
Mutex.new
|
62
|
+
::Thread::Mutex.new
|
61
63
|
else
|
62
|
-
NoopMutex.new
|
64
|
+
::LRUHash::NoopMutex.new
|
63
65
|
end
|
64
66
|
end
|
65
67
|
|
@@ -83,7 +85,7 @@ class LRUHash
|
|
83
85
|
@lock.synchronize do
|
84
86
|
@table.delete(key)
|
85
87
|
@table[key] = resource
|
86
|
-
resource.updated_at =
|
88
|
+
resource.updated_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
87
89
|
end
|
88
90
|
clear_unused_resources if @table.length > @max_size
|
89
91
|
end
|
@@ -98,7 +100,7 @@ class LRUHash
|
|
98
100
|
found = @table.delete(key)
|
99
101
|
if found
|
100
102
|
@table[key] = found
|
101
|
-
found.updated_at =
|
103
|
+
found.updated_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
102
104
|
end
|
103
105
|
found
|
104
106
|
end
|
@@ -130,9 +132,10 @@ class LRUHash
|
|
130
132
|
timer_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
131
133
|
|
132
134
|
ran = try_synchronize do
|
133
|
-
# Clears resources that have not been used
|
135
|
+
# Clears resources that have not been used
|
136
|
+
# in the last 5 minutes (default value of Semian.minimum_lru_time).
|
134
137
|
|
135
|
-
stop_time =
|
138
|
+
stop_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @min_time
|
136
139
|
@table.each do |_, resource|
|
137
140
|
payload[:examined] += 1
|
138
141
|
|
data/lib/semian/platform.rb
CHANGED
@@ -5,8 +5,14 @@ module Semian
|
|
5
5
|
extend Forwardable
|
6
6
|
|
7
7
|
def_delegators :@bulkhead, :destroy, :count, :semid, :tickets, :registered_workers
|
8
|
-
def_delegators :@circuit_breaker,
|
9
|
-
:
|
8
|
+
def_delegators :@circuit_breaker,
|
9
|
+
:reset,
|
10
|
+
:mark_failed,
|
11
|
+
:mark_success,
|
12
|
+
:request_allowed?,
|
13
|
+
:open?,
|
14
|
+
:closed?,
|
15
|
+
:half_open?
|
10
16
|
|
11
17
|
attr_reader :bulkhead, :circuit_breaker, :name
|
12
18
|
attr_accessor :updated_at
|
@@ -15,7 +21,7 @@ module Semian
|
|
15
21
|
@name = name
|
16
22
|
@bulkhead = bulkhead
|
17
23
|
@circuit_breaker = circuit_breaker
|
18
|
-
@updated_at =
|
24
|
+
@updated_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
19
25
|
end
|
20
26
|
|
21
27
|
def destroy
|
data/lib/semian/version.rb
CHANGED
data/lib/semian.rb
CHANGED
@@ -104,7 +104,7 @@ module Semian
|
|
104
104
|
attr_accessor :maximum_lru_size, :minimum_lru_time, :default_permissions, :namespace
|
105
105
|
|
106
106
|
self.maximum_lru_size = 500
|
107
|
-
self.minimum_lru_time = 300
|
107
|
+
self.minimum_lru_time = 300 # 300 seconds / 5 minutes
|
108
108
|
self.default_permissions = 0660
|
109
109
|
|
110
110
|
def issue_disabled_semaphores_warning
|
@@ -122,11 +122,16 @@ module Semian
|
|
122
122
|
attr_accessor :semian_identifier
|
123
123
|
|
124
124
|
def to_s
|
125
|
+
message = super
|
125
126
|
if @semian_identifier
|
126
|
-
"[#{@semian_identifier}]
|
127
|
-
|
128
|
-
|
127
|
+
prefix = "[#{@semian_identifier}] "
|
128
|
+
# When an error is created from another error's message it might
|
129
|
+
# already have a semian identifier in their message
|
130
|
+
unless message.start_with?(prefix)
|
131
|
+
message = "#{prefix}#{message}"
|
132
|
+
end
|
129
133
|
end
|
134
|
+
message
|
130
135
|
end
|
131
136
|
end
|
132
137
|
|
@@ -156,11 +161,17 @@ module Semian
|
|
156
161
|
#
|
157
162
|
# +timeout+: Default timeout in seconds. Default 0. (bulkhead)
|
158
163
|
#
|
164
|
+
# +error_timeout+: The duration in seconds since the last error after which the error count is reset to 0.
|
165
|
+
# (circuit breaker required)
|
166
|
+
#
|
159
167
|
# +error_threshold+: The amount of errors that must happen within error_timeout amount of time to open
|
160
168
|
# the circuit. (circuit breaker required)
|
161
169
|
#
|
162
|
-
# +
|
163
|
-
# (circuit breaker
|
170
|
+
# +error_threshold_timeout+: The duration in seconds to examine number of errors to compare with error_threshold.
|
171
|
+
# Default same as error_timeout. (circuit breaker)
|
172
|
+
#
|
173
|
+
# +error_threshold_timeout_enabled+: flag to enable/disable filter time window based error eviction
|
174
|
+
# (error_threshold_timeout). Default true. (circuit breaker)
|
164
175
|
#
|
165
176
|
# +success_threshold+: The number of consecutive success after which an half-open circuit will be fully closed.
|
166
177
|
# (circuit breaker required)
|
@@ -170,6 +181,8 @@ module Semian
|
|
170
181
|
#
|
171
182
|
# Returns the registered resource.
|
172
183
|
def register(name, **options)
|
184
|
+
return UnprotectedResource.new(name) if ENV.key?("SEMIAN_DISABLED")
|
185
|
+
|
173
186
|
circuit_breaker = create_circuit_breaker(name, **options)
|
174
187
|
bulkhead = create_bulkhead(name, **options)
|
175
188
|
|
@@ -264,8 +277,8 @@ module Semian
|
|
264
277
|
private
|
265
278
|
|
266
279
|
def create_circuit_breaker(name, **options)
|
267
|
-
|
268
|
-
return unless circuit_breaker
|
280
|
+
return if ENV.key?("SEMIAN_CIRCUIT_BREAKER_DISABLED")
|
281
|
+
return unless options.fetch(:circuit_breaker, true)
|
269
282
|
|
270
283
|
require_keys!([:success_threshold, :error_threshold, :error_timeout], options)
|
271
284
|
|
@@ -304,16 +317,18 @@ module Semian
|
|
304
317
|
end
|
305
318
|
|
306
319
|
def create_bulkhead(name, **options)
|
307
|
-
|
308
|
-
return unless bulkhead
|
320
|
+
return if ENV.key?("SEMIAN_BULKHEAD_DISABLED")
|
321
|
+
return unless options.fetch(:bulkhead, true)
|
309
322
|
|
310
323
|
permissions = options[:permissions] || default_permissions
|
311
324
|
timeout = options[:timeout] || 0
|
312
|
-
::Semian::Resource.new(
|
325
|
+
::Semian::Resource.new(
|
326
|
+
name,
|
313
327
|
tickets: options[:tickets],
|
314
328
|
quota: options[:quota],
|
315
329
|
permissions: permissions,
|
316
|
-
timeout: timeout
|
330
|
+
timeout: timeout,
|
331
|
+
)
|
317
332
|
end
|
318
333
|
|
319
334
|
def require_keys!(required, options)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: semian
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.17.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Scott Francis
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2023-02-08 00:00:00.000000000 Z
|
14
14
|
dependencies: []
|
15
15
|
description: |2
|
16
16
|
A Ruby C extention that is used to control access to shared resources
|