semian_extension 0.11.4.1

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.
@@ -0,0 +1,271 @@
1
+ #include "sysv_semaphores.h"
2
+ #include <time.h>
3
+
4
+ static key_t
5
+ generate_key(const char *name);
6
+
7
+ static void *
8
+ acquire_semaphore(void *p);
9
+
10
+ static int
11
+ wait_for_new_semaphore_set(key_t key, long permissions);
12
+
13
+ static void
14
+ initialize_new_semaphore_values(int sem_id, long permissions);
15
+
16
+ static long
17
+ diff_timespec_ms(struct timespec *end, struct timespec *begin);
18
+
19
+ // Generate string rep for sem indices for debugging puproses
20
+ static const char *SEMINDEX_STRING[] = {
21
+ FOREACH_SEMINDEX(GENERATE_STRING)
22
+ };
23
+
24
+ void
25
+ raise_semian_syscall_error(const char *syscall, int error_num)
26
+ {
27
+ rb_raise(eSyscall, "%s failed, errno: %d (%s)", syscall, error_num, strerror(error_num));
28
+ }
29
+
30
+ void
31
+ initialize_semaphore_set(semian_resource_t* res, const char* id_str, long permissions, int tickets, double quota)
32
+ {
33
+
34
+ res->key = generate_key(id_str);
35
+ res->strkey = (char*) malloc((2 /*for 0x*/+ sizeof(uint64_t) /*actual key*/+ 1 /*null*/) * sizeof(char));
36
+ sprintf(res->strkey, "0x%08x", (unsigned int) res->key);
37
+ res->sem_id = semget(res->key, SI_NUM_SEMAPHORES, IPC_CREAT | IPC_EXCL | permissions);
38
+
39
+ /*
40
+ This approach is based on http://man7.org/tlpi/code/online/dist/svsem/svsem_good_init.c.html
41
+ which avoids race conditions when initializing semaphore sets.
42
+ */
43
+ if (res->sem_id != -1) {
44
+ // Happy path - we are the first worker, initialize the semaphore set.
45
+ initialize_new_semaphore_values(res->sem_id, permissions);
46
+ } else {
47
+ // Something went wrong
48
+ if (errno != EEXIST) {
49
+ raise_semian_syscall_error("semget() failed to initialize semaphore values", errno);
50
+ } else {
51
+ // The semaphore set already exists, ensure it is initialized
52
+ res->sem_id = wait_for_new_semaphore_set(res->key, permissions);
53
+ }
54
+ }
55
+
56
+ set_semaphore_permissions(res->sem_id, permissions);
57
+
58
+ /*
59
+ Ensure that a worker for this process is registered.
60
+ Note that from ruby we ensure that at most one worker may be registered per process.
61
+ */
62
+ if (perform_semop(res->sem_id, SI_SEM_REGISTERED_WORKERS, 1, SEM_UNDO, NULL) == -1) {
63
+ rb_raise(eInternal, "error incrementing registered workers, errno: %d (%s)", errno, strerror(errno));
64
+ }
65
+
66
+ int state = 0;
67
+ sem_meta_lock(res->sem_id); // Sets otime for the first time by acquiring the sem lock
68
+
69
+ configure_tickets_args_t configure_tickets_args = (configure_tickets_args_t){
70
+ .sem_id = res->sem_id,
71
+ .tickets = tickets,
72
+ .quota = quota,
73
+ };
74
+ rb_protect(
75
+ configure_tickets,
76
+ (VALUE)&configure_tickets_args,
77
+ &state);
78
+
79
+ sem_meta_unlock(res->sem_id);
80
+ if (state) {
81
+ rb_jump_tag(state);
82
+ }
83
+ }
84
+
85
+ void
86
+ set_semaphore_permissions(int sem_id, long permissions)
87
+ {
88
+ union semun sem_opts;
89
+ struct semid_ds stat_buf;
90
+
91
+ sem_opts.buf = &stat_buf;
92
+ semctl(sem_id, 0, IPC_STAT, sem_opts);
93
+ if ((stat_buf.sem_perm.mode & 0xfff) != permissions) {
94
+ stat_buf.sem_perm.mode &= ~0xfff;
95
+ stat_buf.sem_perm.mode |= permissions;
96
+ semctl(sem_id, 0, IPC_SET, sem_opts);
97
+ }
98
+ }
99
+
100
+ int
101
+ perform_semop(int sem_id, short index, short op, short flags, struct timespec *ts)
102
+ {
103
+ int result;
104
+ struct sembuf buf = { 0 };
105
+
106
+ buf.sem_num = index;
107
+ buf.sem_op = op;
108
+ buf.sem_flg = flags;
109
+
110
+ int num_retries = 3;
111
+
112
+ do {
113
+ result = semtimedop(sem_id, &buf, 1, ts);
114
+ } while (result < 0 && errno == EINTR && num_retries-- > 0);
115
+
116
+ return result;
117
+ }
118
+
119
+ int
120
+ get_sem_val(int sem_id, int sem_index)
121
+ {
122
+ int ret = semctl(sem_id, sem_index, GETVAL);
123
+ if (ret == -1) {
124
+ rb_raise(eInternal, "error getting value of %s for sem %d, errno: %d (%s)", SEMINDEX_STRING[sem_index], sem_id, errno, strerror(errno));
125
+ }
126
+ return ret;
127
+ }
128
+
129
+ void
130
+ sem_meta_lock(int sem_id)
131
+ {
132
+ struct timespec ts = { 0 };
133
+ ts.tv_sec = INTERNAL_TIMEOUT;
134
+
135
+ if (perform_semop(sem_id, SI_SEM_LOCK, -1, SEM_UNDO, &ts) == -1) {
136
+ raise_semian_syscall_error("error acquiring internal semaphore lock, semtimedop()", errno);
137
+ }
138
+ }
139
+
140
+ void
141
+ sem_meta_unlock(int sem_id)
142
+ {
143
+ if (perform_semop(sem_id, SI_SEM_LOCK, 1, SEM_UNDO, NULL) == -1) {
144
+ raise_semian_syscall_error("error releasing internal semaphore lock, semop()", errno);
145
+ }
146
+ }
147
+
148
+ int
149
+ get_semaphore(int key)
150
+ {
151
+ return semget(key, SI_NUM_SEMAPHORES, 0);
152
+ }
153
+
154
+ void *
155
+ acquire_semaphore_without_gvl(void *p)
156
+ {
157
+ WITHOUT_GVL(acquire_semaphore, p, RUBY_UBF_IO, NULL);
158
+ return NULL;
159
+ }
160
+
161
+ static void *
162
+ acquire_semaphore(void *p)
163
+ {
164
+ semian_resource_t *res = (semian_resource_t *) p;
165
+ res->error = 0;
166
+ res->wait_time = -1;
167
+ #ifdef DEBUG
168
+ print_sem_vals(res->sem_id);
169
+ #endif
170
+
171
+ struct timespec begin, end;
172
+ int benchmark_result = clock_gettime(CLOCK_MONOTONIC, &begin);
173
+ if (perform_semop(res->sem_id, SI_SEM_TICKETS, -1, SEM_UNDO, &res->timeout) == -1) {
174
+ res->error = errno;
175
+ }
176
+ if (benchmark_result == 0) {
177
+ if (clock_gettime(CLOCK_MONOTONIC, &end) == 0) {
178
+ res->wait_time = diff_timespec_ms(&end, &begin);
179
+ }
180
+ }
181
+ return NULL;
182
+ }
183
+
184
+ static key_t
185
+ generate_key(const char *name)
186
+ {
187
+ char semset_size_key[20];
188
+ char *uniq_id_str;
189
+
190
+ // It is necessary for the cardinatily of the semaphore set to be part of the key
191
+ // or else sem_get will complain that we have requested an incorrect number of sems
192
+ // for the desired key, and have changed the number of semaphores for a given key
193
+ sprintf(semset_size_key, "_NUM_SEMS_%d", SI_NUM_SEMAPHORES);
194
+ uniq_id_str = malloc(strlen(name)+strlen(semset_size_key)+1);
195
+ strcpy(uniq_id_str, name);
196
+ strcat(uniq_id_str, semset_size_key);
197
+
198
+ union {
199
+ unsigned char str[SHA_DIGEST_LENGTH];
200
+ key_t key;
201
+ } digest;
202
+ SHA1((const unsigned char *) uniq_id_str, strlen(uniq_id_str), digest.str);
203
+ free(uniq_id_str);
204
+ /* TODO: compile-time assertion that sizeof(key_t) > SHA_DIGEST_LENGTH */
205
+ return digest.key;
206
+ }
207
+
208
+
209
+ static void
210
+ initialize_new_semaphore_values(int sem_id, long permissions)
211
+ {
212
+ unsigned short init_vals[SI_NUM_SEMAPHORES];
213
+
214
+ init_vals[SI_SEM_TICKETS] = init_vals[SI_SEM_CONFIGURED_TICKETS] = 0;
215
+ init_vals[SI_SEM_REGISTERED_WORKERS] = 0;
216
+ init_vals[SI_SEM_LOCK] = 1;
217
+
218
+ if (semctl(sem_id, 0, SETALL, init_vals) == -1) {
219
+ raise_semian_syscall_error("semctl()", errno);
220
+ }
221
+ #ifdef DEBUG
222
+ print_sem_vals(sem_id);
223
+ #endif
224
+ }
225
+
226
+ static int
227
+ wait_for_new_semaphore_set(key_t key, long permissions)
228
+ {
229
+ int i;
230
+ int sem_id = -1;
231
+ union semun sem_opts;
232
+ struct semid_ds sem_ds;
233
+
234
+ sem_opts.buf = &sem_ds;
235
+ sem_id = semget(key, 1, permissions);
236
+
237
+ if (sem_id == -1){
238
+ raise_semian_syscall_error("semget()", errno);
239
+ }
240
+
241
+ for (i = 0; i < ((INTERNAL_TIMEOUT * MICROSECONDS_IN_SECOND) / INIT_WAIT); i++) {
242
+
243
+ if (semctl(sem_id, 0, IPC_STAT, sem_opts) == -1) {
244
+ raise_semian_syscall_error("semctl()", errno);
245
+ }
246
+
247
+ // If a semop has been performed by someone else, the values must be initialized
248
+ if (sem_ds.sem_otime != 0) {
249
+ break;
250
+ }
251
+
252
+ #ifdef DEBUG
253
+ printf("Waiting for another process to initialize semaphore values, checked: %d times\n", i);
254
+ #endif
255
+ usleep(INIT_WAIT);
256
+ }
257
+
258
+ if (sem_ds.sem_otime == 0) {
259
+ rb_raise(eTimeout, "error: timeout waiting for semaphore values to initialize after %d checks", INTERNAL_TIMEOUT);
260
+ }
261
+
262
+ return sem_id;
263
+ }
264
+
265
+ static long
266
+ diff_timespec_ms(struct timespec *end, struct timespec *begin)
267
+ {
268
+ long end_ms = (end->tv_sec * 1e3) + (end->tv_nsec / 1e6);
269
+ long begin_ms = (begin->tv_sec * 1e3) + (begin->tv_nsec / 1e6);
270
+ return end_ms - begin_ms;
271
+ }
@@ -0,0 +1,122 @@
1
+ /*
2
+ For manipulating the semian's semaphore set
3
+
4
+ Semian semaphore operations and initialization,
5
+ and functions associated directly weth semops.
6
+ */
7
+ #ifndef SEMIAN_SEMSET_H
8
+ #define SEMIAN_SEMSET_H
9
+
10
+ #include <errno.h>
11
+ #include <stdio.h>
12
+ #include <string.h>
13
+
14
+ #include <openssl/sha.h>
15
+ #include <ruby.h>
16
+ #include <ruby/util.h>
17
+ #include <ruby/io.h>
18
+
19
+ #include "types.h"
20
+ #include "tickets.h"
21
+
22
+ // Defines for ruby threading primitives
23
+ #if defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL) && defined(HAVE_RUBY_THREAD_H)
24
+ // 2.0
25
+ #include <ruby/thread.h>
26
+ #define WITHOUT_GVL(fn,a,ubf,b) rb_thread_call_without_gvl((fn),(a),(ubf),(b))
27
+ #elif defined(HAVE_RB_THREAD_BLOCKING_REGION)
28
+ // 1.9
29
+ typedef VALUE (*my_blocking_fn_t)(void*);
30
+ #define WITHOUT_GVL(fn,a,ubf,b) rb_thread_blocking_region((my_blocking_fn_t)(fn),(a),(ubf),(b))
31
+ #endif
32
+
33
+ // Time to wait for timed ops to complete
34
+ #define INTERNAL_TIMEOUT 5 /* seconds */
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
+
66
+ extern VALUE eSyscall, eTimeout, eInternal;
67
+
68
+ // Helper for syscall verbose debugging
69
+ void
70
+ raise_semian_syscall_error(const char *syscall, int error_num);
71
+
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);
75
+
76
+ // Set semaphore UNIX octal permissions
77
+ void
78
+ set_semaphore_permissions(int sem_id, long permissions);
79
+
80
+ // Create a new sysV IPC semaphore set
81
+ int
82
+ create_semaphore(int key, long permissions, int *created);
83
+
84
+ // Wrapper to performs a semop call
85
+ // The call may be timed or untimed
86
+ int
87
+ perform_semop(int sem_id, short index, short op, short flags, struct timespec *ts);
88
+
89
+ // Retrieve the current number of tickets in a semaphore by its semaphore index
90
+ int
91
+ get_sem_val(int sem_id, int sem_index);
92
+
93
+ // Obtain an exclusive lock on the semaphore set critical section
94
+ void
95
+ sem_meta_lock(int sem_id);
96
+
97
+ // Release an exclusive lock on the semaphore set critical section
98
+ void
99
+ sem_meta_unlock(int sem_id);
100
+
101
+ // Retrieve a semaphore's ID from its key
102
+ int
103
+ get_semaphore(int key);
104
+
105
+ // Decrements the ticket semaphore within the semaphore set
106
+ void *
107
+ acquire_semaphore_without_gvl(void *p);
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
+
122
+ #endif // SEMIAN_SEMSET_H
@@ -0,0 +1,76 @@
1
+ #include "tickets.h"
2
+
3
+ // Update the ticket count for static ticket tracking
4
+ static VALUE
5
+ update_ticket_count(int sem_id, int count);
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
+ VALUE
12
+ configure_tickets(VALUE value)
13
+ {
14
+ configure_tickets_args_t *args = (configure_tickets_args_t *)value;
15
+
16
+ if (args->quota > 0) {
17
+ args->tickets = calculate_quota_tickets(args->sem_id, args->quota);
18
+ }
19
+
20
+ /*
21
+ A manually specified ticket count of 0 is special, meaning "don't set"
22
+ We need to throw an error if we set it to 0 during initialization.
23
+ Otherwise, we back out of here completely.
24
+ */
25
+ if (get_sem_val(args->sem_id, SI_SEM_CONFIGURED_TICKETS) == 0 && args->tickets == 0) {
26
+ rb_raise(eSyscall, "More than 0 tickets must be specified when initializing semaphore");
27
+ } else if (args->tickets == 0) {
28
+ return Qnil;
29
+ }
30
+
31
+ /*
32
+ If the current configured ticket count is not the same as the requested ticket
33
+ count, we need to resize the count. We do this by adding the delta of
34
+ (tickets - current_configured_tickets) to the semaphore value.
35
+ */
36
+ if (get_sem_val(args->sem_id, SI_SEM_CONFIGURED_TICKETS) != args->tickets) {
37
+ update_ticket_count(args->sem_id, args->tickets);
38
+ }
39
+
40
+ return Qnil;
41
+ }
42
+
43
+ static VALUE
44
+ update_ticket_count(int sem_id, int tickets)
45
+ {
46
+ short delta;
47
+ struct timespec ts = { 0 };
48
+ ts.tv_sec = INTERNAL_TIMEOUT;
49
+
50
+ delta = tickets - get_sem_val(sem_id, SI_SEM_CONFIGURED_TICKETS);
51
+
52
+ #ifdef DEBUG
53
+ print_sem_vals(sem_id);
54
+ #endif
55
+ if (perform_semop(sem_id, SI_SEM_TICKETS, delta, 0, &ts) == -1) {
56
+ if (delta < 0 && errno == EAGAIN) {
57
+ rb_raise(eTimeout, "timeout while trying to update ticket count");
58
+ } else {
59
+ rb_raise(eInternal, "error setting ticket count, errno: %d (%s)", errno, strerror(errno));
60
+ }
61
+ }
62
+
63
+ if (semctl(sem_id, SI_SEM_CONFIGURED_TICKETS, SETVAL, tickets) == -1) {
64
+ rb_raise(eInternal, "error configuring ticket count, errno: %d (%s)", errno, strerror(errno));
65
+ }
66
+
67
+ return Qnil;
68
+ }
69
+
70
+ static int
71
+ calculate_quota_tickets (int sem_id, double quota)
72
+ {
73
+ int tickets = 0;
74
+ tickets = (int) ceil(get_sem_val(sem_id, SI_SEM_REGISTERED_WORKERS) * quota);
75
+ return tickets;
76
+ }
@@ -0,0 +1,13 @@
1
+ /*
2
+ For logic specific to manipulating semian ticket counts
3
+ */
4
+ #ifndef SEMIAN_TICKETS_H
5
+ #define SEMIAN_TICKETS_H
6
+
7
+ #include "sysv_semaphores.h"
8
+
9
+ // Set initial ticket values upon resource creation
10
+ VALUE
11
+ configure_tickets(VALUE);
12
+
13
+ #endif // SEMIAN_TICKETS_H
@@ -0,0 +1,41 @@
1
+ /*
2
+ For custom type definitions specific to semian
3
+ */
4
+ #ifndef SEMIAN_TYPES_H
5
+ #define SEMIAN_TYPES_H
6
+
7
+ #include <stdint.h>
8
+ #include <sys/types.h>
9
+ #include <sys/ipc.h>
10
+ #include <sys/sem.h>
11
+ #include <sys/time.h>
12
+
13
+ // For sysV semop syscals
14
+ // see man semop
15
+ union semun {
16
+ int val; /* Value for SETVAL */
17
+ struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
18
+ unsigned short *array; /* Array for GETALL, SETALL */
19
+ struct seminfo *__buf; /* Buffer for IPC_INFO
20
+ (Linux-specific) */
21
+ };
22
+
23
+ typedef struct {
24
+ int sem_id;
25
+ int tickets;
26
+ double quota;
27
+ } configure_tickets_args_t;
28
+
29
+ // Internal semaphore structure
30
+ typedef struct {
31
+ int sem_id;
32
+ struct timespec timeout;
33
+ double quota;
34
+ int error;
35
+ uint64_t key;
36
+ char *strkey;
37
+ char *name;
38
+ long wait_time;
39
+ } semian_resource_t;
40
+
41
+ #endif // SEMIAN_TYPES_H
@@ -0,0 +1,75 @@
1
+ require 'semian'
2
+
3
+ module Semian
4
+ module Adapter
5
+ def semian_identifier
6
+ raise NotImplementedError.new("Semian adapters must implement a `semian_identifier` method")
7
+ end
8
+
9
+ def semian_resource
10
+ @semian_resource ||= case semian_options
11
+ when false
12
+ UnprotectedResource.new(semian_identifier)
13
+ when nil
14
+ Semian.logger.info("Semian is not configured for #{self.class.name}: #{semian_identifier}")
15
+ UnprotectedResource.new(semian_identifier)
16
+ else
17
+ options = semian_options.dup
18
+ options.delete(:name)
19
+ options[:consumer] = self
20
+ options[:exceptions] ||= []
21
+ options[:exceptions] += resource_exceptions
22
+ ::Semian.retrieve_or_register(semian_identifier, **options)
23
+ end
24
+ end
25
+
26
+ def clear_semian_resource
27
+ @semian_resource = nil
28
+ end
29
+
30
+ private
31
+
32
+ def acquire_semian_resource(scope:, adapter:, &block)
33
+ return yield if resource_already_acquired?
34
+ semian_resource.acquire(scope: scope, adapter: adapter, resource: self) do
35
+ mark_resource_as_acquired(&block)
36
+ end
37
+ rescue ::Semian::OpenCircuitError => error
38
+ last_error = semian_resource.circuit_breaker.last_error
39
+ message = "#{error.message} caused by #{last_error.message}"
40
+ last_error = nil unless last_error.is_a?(Exception) # Net::HTTPServerError is not an exception
41
+ raise self.class::CircuitOpenError.new(semian_identifier, message), cause: last_error
42
+ rescue ::Semian::BaseError => error
43
+ raise self.class::ResourceBusyError.new(semian_identifier, error.message)
44
+ rescue *resource_exceptions => error
45
+ error.semian_identifier = semian_identifier if error.respond_to?(:semian_identifier=)
46
+ raise
47
+ end
48
+
49
+ def semian_options
50
+ return @semian_options if defined? @semian_options
51
+ options = raw_semian_options
52
+ @semian_options = options && options.map { |k, v| [k.to_sym, v] }.to_h
53
+ end
54
+
55
+ def raw_semian_options
56
+ raise NotImplementedError.new("Semian adapters must implement a `raw_semian_options` method")
57
+ end
58
+
59
+ def resource_exceptions
60
+ []
61
+ end
62
+
63
+ def resource_already_acquired?
64
+ @resource_acquired
65
+ end
66
+
67
+ def mark_resource_as_acquired
68
+ previous = @resource_acquired
69
+ @resource_acquired = true
70
+ yield
71
+ ensure
72
+ @resource_acquired = previous
73
+ end
74
+ end
75
+ end