semian 0.6.2 → 0.7.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/ext/semian/extconf.rb +1 -1
- data/ext/semian/resource.c +134 -17
- data/ext/semian/resource.h +56 -3
- data/ext/semian/semian.c +6 -1
- data/ext/semian/sysv_semaphores.c +148 -43
- data/ext/semian/sysv_semaphores.h +49 -5
- data/ext/semian/tickets.c +64 -54
- data/ext/semian/tickets.h +2 -6
- data/ext/semian/types.h +4 -9
- data/lib/semian.rb +111 -17
- data/lib/semian/adapter.rb +5 -0
- data/lib/semian/circuit_breaker.rb +9 -7
- data/lib/semian/mysql2.rb +2 -0
- data/lib/semian/protected_resource.rb +37 -14
- data/lib/semian/redis.rb +8 -6
- data/lib/semian/resource.rb +27 -3
- data/lib/semian/simple_integer.rb +15 -0
- data/lib/semian/simple_sliding_window.rb +35 -10
- data/lib/semian/simple_state.rb +7 -0
- data/lib/semian/unprotected_resource.rb +12 -0
- data/lib/semian/version.rb +1 -1
- metadata +3 -2
@@ -16,7 +16,8 @@ and functions associated directly weth semops.
|
|
16
16
|
#include <ruby/util.h>
|
17
17
|
#include <ruby/io.h>
|
18
18
|
|
19
|
-
#include
|
19
|
+
#include "types.h"
|
20
|
+
#include "tickets.h"
|
20
21
|
|
21
22
|
// Defines for ruby threading primitives
|
22
23
|
#if defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL) && defined(HAVE_RUBY_THREAD_H)
|
@@ -32,15 +33,45 @@ typedef VALUE (*my_blocking_fn_t)(void*);
|
|
32
33
|
// Time to wait for timed ops to complete
|
33
34
|
#define INTERNAL_TIMEOUT 5 /* seconds */
|
34
35
|
|
36
|
+
// Time to wait while checking if semaphore has been initialized
|
37
|
+
#define INIT_WAIT 10 /* microseconds */
|
38
|
+
|
39
|
+
// Helper definition to prevent magic number for conversion of microseconds to seconds
|
40
|
+
#define MICROSECONDS_IN_SECOND 1000000
|
41
|
+
|
42
|
+
// Here we define an enum value and string representation of each semaphore
|
43
|
+
// This allows us to key the sem value and string rep in sync easily
|
44
|
+
// utilizing pre-processor macros.
|
45
|
+
// If you're unfamiliar with this pattern, this is using "x macros"
|
46
|
+
// SI_SEM_LOCK metadata lock to act as a mutex, ensuring thread-safety for updating other semaphores
|
47
|
+
// SI_SEM_TICKETS semaphore for the tickets currently issued
|
48
|
+
// SI_SEM_CONFIGURED_TICKETS semaphore to track the desired number of tickets available for issue
|
49
|
+
// SI_SEM_REGISTERED_WORKERS semaphore for the number of workers currently registered
|
50
|
+
// SI_NUM_SEMAPHORES always leave this as last entry for count to be accurate
|
51
|
+
#define FOREACH_SEMINDEX(SEMINDEX) \
|
52
|
+
SEMINDEX(SI_SEM_LOCK) \
|
53
|
+
SEMINDEX(SI_SEM_TICKETS) \
|
54
|
+
SEMINDEX(SI_SEM_CONFIGURED_TICKETS) \
|
55
|
+
SEMINDEX(SI_SEM_REGISTERED_WORKERS) \
|
56
|
+
SEMINDEX(SI_NUM_SEMAPHORES) \
|
57
|
+
|
58
|
+
#define GENERATE_ENUM(ENUM) ENUM,
|
59
|
+
#define GENERATE_STRING(STRING) #STRING,
|
60
|
+
|
61
|
+
// Generate enum for sem indices
|
62
|
+
enum SEMINDEX_ENUM {
|
63
|
+
FOREACH_SEMINDEX(GENERATE_ENUM)
|
64
|
+
};
|
65
|
+
|
35
66
|
VALUE eSyscall, eTimeout, eInternal;
|
36
67
|
|
37
68
|
// Helper for syscall verbose debugging
|
38
69
|
void
|
39
70
|
raise_semian_syscall_error(const char *syscall, int error_num);
|
40
71
|
|
41
|
-
//
|
42
|
-
|
43
|
-
|
72
|
+
// Initialize the sysv semaphore structure
|
73
|
+
void
|
74
|
+
initialize_semaphore_set(semian_resource_t* res, const char* id_str, long permissions, int tickets, double quota);
|
44
75
|
|
45
76
|
// Set semaphore UNIX octal permissions
|
46
77
|
void
|
@@ -57,7 +88,7 @@ perform_semop(int sem_id, short index, short op, short flags, struct timespec *t
|
|
57
88
|
|
58
89
|
// Retrieve the current number of tickets in a semaphore by its semaphore index
|
59
90
|
int
|
60
|
-
|
91
|
+
get_sem_val(int sem_id, int sem_index);
|
61
92
|
|
62
93
|
// Obtain an exclusive lock on the semaphore set critical section
|
63
94
|
void
|
@@ -75,4 +106,17 @@ get_semaphore(int key);
|
|
75
106
|
void *
|
76
107
|
acquire_semaphore_without_gvl(void *p);
|
77
108
|
|
109
|
+
#ifdef DEBUG
|
110
|
+
static inline void
|
111
|
+
print_sem_vals(int sem_id)
|
112
|
+
{
|
113
|
+
printf("lock %d, tickets: %d configured: %d, registered workers %d\n",
|
114
|
+
get_sem_val(sem_id, SI_SEM_LOCK),
|
115
|
+
get_sem_val(sem_id, SI_SEM_TICKETS),
|
116
|
+
get_sem_val(sem_id, SI_SEM_CONFIGURED_TICKETS),
|
117
|
+
get_sem_val(sem_id, SI_SEM_REGISTERED_WORKERS)
|
118
|
+
);
|
119
|
+
}
|
120
|
+
#endif
|
121
|
+
|
78
122
|
#endif // SEMIAN_SEMSET_H
|
data/ext/semian/tickets.c
CHANGED
@@ -1,72 +1,82 @@
|
|
1
|
-
#include
|
1
|
+
#include "tickets.h"
|
2
2
|
|
3
|
-
|
3
|
+
// Update the ticket count for static ticket tracking
|
4
|
+
static VALUE
|
5
|
+
update_ticket_count(update_ticket_count_t *tc);
|
6
|
+
|
7
|
+
static int
|
8
|
+
calculate_quota_tickets(int sem_id, double quota);
|
9
|
+
|
10
|
+
// Must be called with the semaphore meta lock already acquired
|
11
|
+
void
|
12
|
+
configure_tickets(int sem_id, int tickets, double quota)
|
13
|
+
{
|
14
|
+
int state = 0;
|
15
|
+
update_ticket_count_t tc;
|
16
|
+
|
17
|
+
if (quota > 0) {
|
18
|
+
tickets = calculate_quota_tickets(sem_id, quota);
|
19
|
+
}
|
20
|
+
|
21
|
+
/*
|
22
|
+
A manually specified ticket count of 0 is special, meaning "don't set"
|
23
|
+
We need to throw an error if we set it to 0 during initialization.
|
24
|
+
Otherwise, we back out of here completely.
|
25
|
+
*/
|
26
|
+
if (get_sem_val(sem_id, SI_SEM_CONFIGURED_TICKETS) == 0 && tickets == 0) {
|
27
|
+
rb_raise(eSyscall, "More than 0 tickets must be specified when initializing semaphore");
|
28
|
+
} else if (tickets == 0) {
|
29
|
+
return;
|
30
|
+
}
|
31
|
+
|
32
|
+
/*
|
33
|
+
If the current configured ticket count is not the same as the requested ticket
|
34
|
+
count, we need to resize the count. We do this by adding the delta of
|
35
|
+
(tickets - current_configured_tickets) to the semaphore value.
|
36
|
+
*/
|
37
|
+
if (get_sem_val(sem_id, SI_SEM_CONFIGURED_TICKETS) != tickets) {
|
38
|
+
|
39
|
+
tc.sem_id = sem_id;
|
40
|
+
tc.tickets = tickets;
|
41
|
+
rb_protect((VALUE (*)(VALUE)) update_ticket_count, (VALUE) &tc, &state);
|
42
|
+
|
43
|
+
if (state) {
|
44
|
+
rb_jump_tag(state);
|
45
|
+
}
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
static VALUE
|
4
50
|
update_ticket_count(update_ticket_count_t *tc)
|
5
51
|
{
|
6
52
|
short delta;
|
7
53
|
struct timespec ts = { 0 };
|
8
54
|
ts.tv_sec = INTERNAL_TIMEOUT;
|
9
55
|
|
10
|
-
|
11
|
-
delta = tc->tickets - get_max_tickets(tc->sem_id);
|
56
|
+
delta = tc->tickets - get_sem_val(tc->sem_id, SI_SEM_CONFIGURED_TICKETS);
|
12
57
|
|
13
|
-
|
58
|
+
#ifdef DEBUG
|
59
|
+
print_sem_vals(tc->sem_id);
|
60
|
+
#endif
|
61
|
+
if (perform_semop(tc->sem_id, SI_SEM_TICKETS, delta, 0, &ts) == -1) {
|
62
|
+
if (delta < 0 && errno == EAGAIN) {
|
63
|
+
rb_raise(eTimeout, "timeout while trying to update ticket count");
|
64
|
+
} else {
|
14
65
|
rb_raise(eInternal, "error setting ticket count, errno: %d (%s)", errno, strerror(errno));
|
15
66
|
}
|
67
|
+
}
|
16
68
|
|
17
|
-
|
18
|
-
|
19
|
-
}
|
69
|
+
if (semctl(tc->sem_id, SI_SEM_CONFIGURED_TICKETS, SETVAL, tc->tickets) == -1) {
|
70
|
+
rb_raise(eInternal, "error configuring ticket count, errno: %d (%s)", errno, strerror(errno));
|
20
71
|
}
|
21
72
|
|
22
73
|
return Qnil;
|
23
74
|
}
|
24
75
|
|
25
|
-
|
26
|
-
|
76
|
+
static int
|
77
|
+
calculate_quota_tickets (int sem_id, double quota)
|
27
78
|
{
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
int state;
|
32
|
-
|
33
|
-
if (should_initialize) {
|
34
|
-
init_vals[SI_SEM_TICKETS] = init_vals[SI_SEM_CONFIGURED_TICKETS] = tickets;
|
35
|
-
init_vals[SI_SEM_LOCK] = 1;
|
36
|
-
if (semctl(sem_id, 0, SETALL, init_vals) == -1) {
|
37
|
-
raise_semian_syscall_error("semctl()", errno);
|
38
|
-
}
|
39
|
-
} else if (tickets > 0) {
|
40
|
-
/* it's possible that we haven't actually initialized the
|
41
|
-
semaphore structure yet - wait a bit in that case */
|
42
|
-
if (get_max_tickets(sem_id) == 0) {
|
43
|
-
gettimeofday(&start_time, NULL);
|
44
|
-
while (get_max_tickets(sem_id) == 0) {
|
45
|
-
usleep(10000); /* 10ms */
|
46
|
-
gettimeofday(&cur_time, NULL);
|
47
|
-
if ((cur_time.tv_sec - start_time.tv_sec) > INTERNAL_TIMEOUT) {
|
48
|
-
rb_raise(eInternal, "timeout waiting for semaphore initialization");
|
49
|
-
}
|
50
|
-
}
|
51
|
-
}
|
52
|
-
|
53
|
-
/*
|
54
|
-
If the current max ticket count is not the same as the requested ticket
|
55
|
-
count, we need to resize the count. We do this by adding the delta of
|
56
|
-
(tickets - current_max_tickets) to the semaphore value.
|
57
|
-
*/
|
58
|
-
if (get_max_tickets(sem_id) != tickets) {
|
59
|
-
sem_meta_lock(sem_id);
|
60
|
-
|
61
|
-
tc.sem_id = sem_id;
|
62
|
-
tc.tickets = tickets;
|
63
|
-
rb_protect((VALUE (*)(VALUE)) update_ticket_count, (VALUE) &tc, &state);
|
64
|
-
|
65
|
-
sem_meta_unlock(sem_id);
|
66
|
-
|
67
|
-
if (state) {
|
68
|
-
rb_jump_tag(state);
|
69
|
-
}
|
70
|
-
}
|
71
|
-
}
|
79
|
+
int tickets = 0;
|
80
|
+
tickets = (int) ceil(get_sem_val(sem_id, SI_SEM_REGISTERED_WORKERS) * quota);
|
81
|
+
return tickets;
|
72
82
|
}
|
data/ext/semian/tickets.h
CHANGED
@@ -4,14 +4,10 @@ For logic specific to manipulating semian ticket counts
|
|
4
4
|
#ifndef SEMIAN_TICKETS_H
|
5
5
|
#define SEMIAN_TICKETS_H
|
6
6
|
|
7
|
-
#include
|
8
|
-
|
9
|
-
// Update the ticket count for static ticket tracking
|
10
|
-
VALUE
|
11
|
-
update_ticket_count(update_ticket_count_t *tc);
|
7
|
+
#include "sysv_semaphores.h"
|
12
8
|
|
13
9
|
// Set initial ticket values upon resource creation
|
14
10
|
void
|
15
|
-
configure_tickets(int sem_id, int tickets,
|
11
|
+
configure_tickets(int sem_id, int tickets, double quota);
|
16
12
|
|
17
13
|
#endif // SEMIAN_TICKETS_H
|
data/ext/semian/types.h
CHANGED
@@ -4,6 +4,7 @@ For custom type definitions specific to semian
|
|
4
4
|
#ifndef SEMIAN_TYPES_H
|
5
5
|
#define SEMIAN_TYPES_H
|
6
6
|
|
7
|
+
#include <stdint.h>
|
7
8
|
#include <sys/types.h>
|
8
9
|
#include <sys/ipc.h>
|
9
10
|
#include <sys/sem.h>
|
@@ -29,17 +30,11 @@ typedef struct {
|
|
29
30
|
typedef struct {
|
30
31
|
int sem_id;
|
31
32
|
struct timespec timeout;
|
33
|
+
double quota;
|
32
34
|
int error;
|
35
|
+
uint64_t key;
|
36
|
+
char *strkey;
|
33
37
|
char *name;
|
34
38
|
} semian_resource_t;
|
35
39
|
|
36
|
-
// FIXME: move this to more appropriate location once the file exists
|
37
|
-
typedef enum
|
38
|
-
{
|
39
|
-
SI_SEM_TICKETS, // semaphore for the tickets currently issued
|
40
|
-
SI_SEM_CONFIGURED_TICKETS, // semaphore to track the desired number of tickets available for issue
|
41
|
-
SI_SEM_LOCK, // metadata lock to act as a mutex, ensuring thread-safety for updating other semaphores
|
42
|
-
SI_NUM_SEMAPHORES // always leave this as last entry for count to be accurate
|
43
|
-
} semaphore_indices;
|
44
|
-
|
45
40
|
#endif // SEMIAN_TYPES_H
|
data/lib/semian.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'forwardable'
|
2
2
|
require 'logger'
|
3
|
+
require 'weakref'
|
4
|
+
require 'thread'
|
3
5
|
|
4
6
|
require 'semian/version'
|
5
7
|
require 'semian/instrumentable'
|
@@ -77,7 +79,6 @@ require 'semian/simple_state'
|
|
77
79
|
# end
|
78
80
|
#
|
79
81
|
# This is the same as the previous example, but overrides the timeout from the default value of 500 milliseconds to 1 second.
|
80
|
-
#
|
81
82
|
module Semian
|
82
83
|
extend self
|
83
84
|
extend Instrumentable
|
@@ -116,38 +117,58 @@ module Semian
|
|
116
117
|
|
117
118
|
# Registers a resource.
|
118
119
|
#
|
119
|
-
# +name+: Name of the resource - this can be either a string or symbol.
|
120
|
+
# +name+: Name of the resource - this can be either a string or symbol. (required)
|
121
|
+
#
|
122
|
+
# +circuit_breaker+: The boolean if you want a circuit breaker acquired for your resource. Default true.
|
123
|
+
#
|
124
|
+
# +bulkhead+: The boolean if you want a bulkhead to be acquired for your resource. Default true.
|
120
125
|
#
|
121
126
|
# +tickets+: Number of tickets. If this value is 0, the ticket count will not be set,
|
122
127
|
# but the resource must have been previously registered otherwise an error will be raised.
|
128
|
+
# Mutually exclusive with the 'quota' argument.
|
123
129
|
#
|
124
|
-
# +
|
130
|
+
# +quota+: Calculate tickets as a ratio of the number of registered workers.
|
131
|
+
# Must be greater than 0, less than or equal to 1. There will always be at least 1 ticket, as it
|
132
|
+
# is calculated as (workers * quota).ceil
|
133
|
+
# Mutually exclusive with the 'ticket' argument.
|
134
|
+
# but the resource must have been previously registered otherwise an error will be raised. (bulkhead)
|
125
135
|
#
|
126
|
-
# +
|
136
|
+
# +permissions+: Octal permissions of the resource. Default 0660. (bulkhead)
|
127
137
|
#
|
128
|
-
# +
|
138
|
+
# +timeout+: Default timeout in seconds. Default 0. (bulkhead)
|
139
|
+
#
|
140
|
+
# +error_threshold+: The number of errors that will trigger the circuit opening. (circuit breaker required)
|
129
141
|
#
|
130
142
|
# +error_timeout+: The duration in seconds since the last error after which the error count is reset to 0.
|
143
|
+
# (circuit breaker required)
|
131
144
|
#
|
132
145
|
# +success_threshold+: The number of consecutive success after which an half-open circuit will be fully closed.
|
146
|
+
# (circuit breaker required)
|
133
147
|
#
|
134
|
-
# +exceptions+: An array of exception classes that should be accounted as resource errors.
|
148
|
+
# +exceptions+: An array of exception classes that should be accounted as resource errors. Default [].
|
149
|
+
# (circuit breaker)
|
135
150
|
#
|
136
151
|
# Returns the registered resource.
|
137
|
-
def register(name,
|
138
|
-
circuit_breaker =
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
)
|
146
|
-
resource = Resource.new(name, tickets: tickets, permissions: permissions, timeout: timeout)
|
147
|
-
resources[name] = ProtectedResource.new(resource, circuit_breaker)
|
152
|
+
def register(name, **options)
|
153
|
+
circuit_breaker = create_circuit_breaker(name, **options)
|
154
|
+
bulkhead = create_bulkhead(name, **options)
|
155
|
+
|
156
|
+
if circuit_breaker.nil? && bulkhead.nil?
|
157
|
+
raise ArgumentError, 'Both bulkhead and circuitbreaker cannot be disabled.'
|
158
|
+
end
|
159
|
+
|
160
|
+
resources[name] = ProtectedResource.new(name, bulkhead, circuit_breaker)
|
148
161
|
end
|
149
162
|
|
150
163
|
def retrieve_or_register(name, **args)
|
164
|
+
# If consumer who retrieved / registered by a Semian::Adapter, keep track
|
165
|
+
# of who the consumer was so that we can clear the resource reference if needed.
|
166
|
+
if consumer = args.delete(:consumer)
|
167
|
+
if consumer.class.include?(Semian::Adapter)
|
168
|
+
consumers[name] ||= []
|
169
|
+
consumers[name] << WeakRef.new(consumer)
|
170
|
+
end
|
171
|
+
end
|
151
172
|
self[name] || register(name, **args)
|
152
173
|
end
|
153
174
|
|
@@ -162,10 +183,83 @@ module Semian
|
|
162
183
|
end
|
163
184
|
end
|
164
185
|
|
186
|
+
# Unregister will not destroy the semian resource, but it will
|
187
|
+
# remove it from the hash of registered resources, and decrease
|
188
|
+
# the number of registered workers.
|
189
|
+
# Semian.destroy removes the underlying resource, but
|
190
|
+
# Semian.unregister will remove all references, while preserving
|
191
|
+
# the underlying semian resource (and sysV semaphore).
|
192
|
+
# Also clears any semian_resources
|
193
|
+
# in use by any semian adapters if the weak reference is still alive.
|
194
|
+
def unregister(name)
|
195
|
+
if resource = resources.delete(name)
|
196
|
+
resource.bulkhead.unregister_worker if resource.bulkhead
|
197
|
+
consumers_for_resource = consumers.delete(name) || []
|
198
|
+
consumers_for_resource.each do |consumer|
|
199
|
+
begin
|
200
|
+
if consumer.weakref_alive?
|
201
|
+
consumer.clear_semian_resource
|
202
|
+
end
|
203
|
+
rescue WeakRef::RefError
|
204
|
+
next
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Unregisters all resources
|
211
|
+
def unregister_all_resources
|
212
|
+
resources.keys.each do |resource|
|
213
|
+
unregister(resource)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
165
217
|
# Retrieves a hash of all registered resources.
|
166
218
|
def resources
|
167
219
|
@resources ||= {}
|
168
220
|
end
|
221
|
+
|
222
|
+
# Retrieves a hash of all registered resource consumers.
|
223
|
+
def consumers
|
224
|
+
@consumers ||= {}
|
225
|
+
end
|
226
|
+
|
227
|
+
def reset!
|
228
|
+
@consumers = {}
|
229
|
+
@resources = {}
|
230
|
+
end
|
231
|
+
|
232
|
+
private
|
233
|
+
|
234
|
+
def create_circuit_breaker(name, **options)
|
235
|
+
circuit_breaker = options.fetch(:circuit_breaker, true)
|
236
|
+
return unless circuit_breaker
|
237
|
+
raise ArgumentError unless required_keys?([:success_threshold, :error_threshold, :error_timeout], options)
|
238
|
+
implementation = options[:thread_safety_disabled] ? ::Semian::Simple : ::Semian::ThreadSafe
|
239
|
+
|
240
|
+
exceptions = options[:exceptions] || []
|
241
|
+
CircuitBreaker.new(
|
242
|
+
name,
|
243
|
+
success_threshold: options[:success_threshold],
|
244
|
+
error_threshold: options[:error_threshold],
|
245
|
+
error_timeout: options[:error_timeout],
|
246
|
+
exceptions: Array(exceptions) + [::Semian::BaseError],
|
247
|
+
implementation: implementation,
|
248
|
+
)
|
249
|
+
end
|
250
|
+
|
251
|
+
def create_bulkhead(name, **options)
|
252
|
+
bulkhead = options.fetch(:bulkhead, true)
|
253
|
+
return unless bulkhead
|
254
|
+
|
255
|
+
permissions = options[:permissions] || 0660
|
256
|
+
timeout = options[:timeout] || 0
|
257
|
+
Resource.new(name, tickets: options[:tickets], quota: options[:quota], permissions: permissions, timeout: timeout)
|
258
|
+
end
|
259
|
+
|
260
|
+
def required_keys?(required = [], **options)
|
261
|
+
required.all? { |key| options.key? key }
|
262
|
+
end
|
169
263
|
end
|
170
264
|
|
171
265
|
if Semian.semaphores_enabled?
|
data/lib/semian/adapter.rb
CHANGED
@@ -16,12 +16,17 @@ module Semian
|
|
16
16
|
else
|
17
17
|
options = semian_options.dup
|
18
18
|
options.delete(:name)
|
19
|
+
options[:consumer] = self
|
19
20
|
options[:exceptions] ||= []
|
20
21
|
options[:exceptions] += resource_exceptions
|
21
22
|
::Semian.retrieve_or_register(semian_identifier, **options)
|
22
23
|
end
|
23
24
|
end
|
24
25
|
|
26
|
+
def clear_semian_resource
|
27
|
+
@semian_resource = nil
|
28
|
+
end
|
29
|
+
|
25
30
|
private
|
26
31
|
|
27
32
|
def acquire_semian_resource(scope:, adapter:, &block)
|