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.
@@ -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)