semian 0.6.2 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 <types.h>
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
- // Genurates a unique key for the semaphore from the resource id
42
- key_t
43
- generate_key(const char *name);
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
- get_max_tickets(int sem_id);
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
@@ -1,72 +1,82 @@
1
- #include <tickets.h>
1
+ #include "tickets.h"
2
2
 
3
- VALUE
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
- if (get_max_tickets(tc->sem_id) != tc->tickets) {
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
- if (perform_semop(tc->sem_id, SI_SEM_TICKETS, delta, 0, &ts) == -1) {
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
- if (semctl(tc->sem_id, SI_SEM_CONFIGURED_TICKETS, SETVAL, tc->tickets) == -1) {
18
- rb_raise(eInternal, "error updating max ticket count, errno: %d (%s)", errno, strerror(errno));
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
- void
26
- configure_tickets(int sem_id, int tickets, int should_initialize)
76
+ static int
77
+ calculate_quota_tickets (int sem_id, double quota)
27
78
  {
28
- unsigned short init_vals[SI_NUM_SEMAPHORES];
29
- struct timeval start_time, cur_time;
30
- update_ticket_count_t tc;
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
  }
@@ -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 <sysv_semaphores.h>
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, int should_initialize);
11
+ configure_tickets(int sem_id, int tickets, double quota);
16
12
 
17
13
  #endif // SEMIAN_TICKETS_H
@@ -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
@@ -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
- # +permissions+: Octal permissions of the resource.
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
- # +timeout+: Default timeout in seconds.
136
+ # +permissions+: Octal permissions of the resource. Default 0660. (bulkhead)
127
137
  #
128
- # +error_threshold+: The number of errors that will trigger the circuit opening.
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, tickets:, permissions: 0660, timeout: 0, error_threshold:, error_timeout:, success_threshold:, exceptions: [])
138
- circuit_breaker = CircuitBreaker.new(
139
- name,
140
- success_threshold: success_threshold,
141
- error_threshold: error_threshold,
142
- error_timeout: error_timeout,
143
- exceptions: Array(exceptions) + [::Semian::BaseError],
144
- implementation: ::Semian::Simple,
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?
@@ -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)