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