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