semian 0.16.0 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03aa7b6a715e0990279468f71a5bacbd03b94f983c16083570a09dc46c69d325
4
- data.tar.gz: 1b53f85622bfa3868585ea8836df867a37da5b4142b769ad8b968d9865c001c2
3
+ metadata.gz: '0461852ea5a5ffbacaf9a20062213609066e3ef8ade9a93a980d2af73a0feb39'
4
+ data.tar.gz: 8e21023912cd7c71cc74c0eb6a384771018f5fa3f53c212a62c23446801573e2
5
5
  SHA512:
6
- metadata.gz: f7f481a7a77c6d91574500e2a636eb6bd9721e61be5c8740265f58ac90895525ed9b34750b7dbb23d97360a3449a7a990994d870c8a9730e8d06350afda7c7de
7
- data.tar.gz: 216189dea6814080db529843164dd02235541127ce9058ff803cc3843842220e87340b45a73109a2004d6205194b166b0cbcbf55e46601a5bf5f00c75cb5678c
6
+ metadata.gz: 0bd910ad2f7891d4dc2660135adea394db252afa7f53f8c707a55fa569cbf481514af485fe9056d5959e98faffd8bd20990ac9f7b6abe0b0f453ca29d3ad3e9f
7
+ data.tar.gz: a4149e3b4021fe47be636e28bbd5ece3eb4246d65bbc95b07cc50e1b45a328980e47c32d15e81a52e2516fe01b2622fedf31d5aac8bcedbee7e3bdb73a1a0c8c
data/README.md CHANGED
@@ -73,6 +73,7 @@ version is the version of the public gem with the same name:
73
73
  * [`semian/mysql2`][mysql-semian-adapter] (~> 0.3.16)
74
74
  * [`semian/redis`][redis-semian-adapter] (~> 3.2.1)
75
75
  * [`semian/net_http`][nethttp-semian-adapter]
76
+ * [`semian-postgres`][postgres-semian-adapter]
76
77
 
77
78
  ### Creating Adapters
78
79
 
@@ -110,7 +111,7 @@ There are some global configuration options that can be set for Semian:
110
111
  # Note: Setting this to 0 enables aggressive garbage collection.
111
112
  Semian.maximum_lru_size = 0
112
113
 
113
- # Minimum time a resource should be resident in the LRU cache (default: 300s)
114
+ # Minimum time in seconds a resource should be resident in the LRU cache (default: 300s)
114
115
  Semian.minimum_lru_time = 60
115
116
  ```
116
117
 
@@ -418,9 +419,11 @@ response time. This is the problem Semian solves by failing fast.
418
419
 
419
420
  ## How does Semian work?
420
421
 
421
- Semian consists of two parts: **Circuit breaker** and **Bulkheading**. To understand
422
- Semian, and especially how to configure it, we must understand these patterns
423
- and their implementation.
422
+ Semian consists of two parts: **Circuit Breaker** and **Bulkheading**.
423
+ To understand Semian, and especially how to configure it,
424
+ we must understand these patterns and their implementation.
425
+
426
+ Disable Semian via environment variable `SEMIAN_DISABLED=1`.
424
427
 
425
428
  ### Circuit Breaker
426
429
 
@@ -451,18 +454,28 @@ all workers on a server.
451
454
 
452
455
  There are four configuration parameters for circuit breakers in Semian:
453
456
 
454
- * **error_threshold**. The amount of errors a worker encounters within error_threshold_timeout amount of time before
455
- opening the circuit, that is to start rejecting requests instantly.
456
- * **error_threshold_timeout**. The amount of time in seconds that error_threshold errors must occur to open the circuit. Defaults to error_timeout seconds if not set.
457
+ * **circuit_breaker**. Enable or Disable Circuit Breaker. Defaults to `true` if not set.
458
+ * **error_threshold**. The amount of errors a worker encounters within `error_threshold_timeout`
459
+ amount of time before opening the circuit,
460
+ that is to start rejecting requests instantly.
461
+ * **error_threshold_timeout**. The amount of time in seconds that `error_threshold`
462
+ errors must occur to open the circuit.
463
+ Defaults to `error_timeout` seconds if not set.
457
464
  * **error_timeout**. The amount of time in seconds until trying to query the resource
458
465
  again.
459
- * **error_threshold_timeout_enabled**. If set to false it will disable the time window for evicting old exceptions. `error_timeout` is still used and will reset
460
- the circuit. Defaults to `true` if not set.
466
+ * **error_threshold_timeout_enabled**. If set to false it will disable
467
+ the time window for evicting old exceptions. `error_timeout` is still used and
468
+ will reset the circuit. Defaults to `true` if not set.
461
469
  * **success_threshold**. The amount of successes on the circuit until closing it
462
470
  again, that is to start accepting all requests to the circuit.
463
- * **half_open_resource_timeout**. Timeout for the resource in seconds when the circuit is half-open (supported for MySQL, Net::HTTP and Redis).
471
+ * **half_open_resource_timeout**. Timeout for the resource in seconds when
472
+ the circuit is half-open (supported for MySQL, Net::HTTP and Redis).
473
+
474
+ It is possible to disable Circuit Breaker with environment variable
475
+ `SEMIAN_CIRCUIT_BREAKER_DISABLED=1`.
464
476
 
465
- For more information about configuring these parameters, please read [this post](https://engineering.shopify.com/blogs/engineering/circuit-breaker-misconfigured).
477
+ For more information about configuring these parameters, please read
478
+ [this post](https://engineering.shopify.com/blogs/engineering/circuit-breaker-misconfigured).
466
479
 
467
480
  ### Bulkheading
468
481
 
@@ -515,11 +528,15 @@ still experimenting with ways to figure out optimal ticket numbers. Generally
515
528
  something below half the number of workers on the server for endpoints that are
516
529
  queried frequently has worked well for us.
517
530
 
531
+ * **bulkhead**. Enable or Disable Bulkhead. Defaults to `true` if not set.
518
532
  * **tickets**. Number of workers that can concurrently access a resource.
519
533
  * **timeout**. Time to wait in seconds to acquire a ticket if there are no tickets left.
520
534
  We recommend this to be `0` unless you have very few workers running (i.e.
521
535
  less than ~5).
522
536
 
537
+ It is possible to disable Bulkhead with environment variable
538
+ `SEMIAN_BULKHEAD_DISABLED=1`.
539
+
523
540
  Note that there are system-wide limitations on how many tickets can be allocated
524
541
  on a system. `cat /proc/sys/kernel/sem` will tell you.
525
542
 
@@ -549,7 +566,6 @@ ipcs -si $(ipcs -s | grep 0x48af51ea | awk '{print $2}')
549
566
  Which should output something like:
550
567
 
551
568
  ```
552
-
553
569
  Semaphore Array semid=5570729
554
570
  uid=8192 gid=8192 cuid=8192 cgid=8192
555
571
  mode=0660, access_perms=0660
@@ -841,6 +857,7 @@ $ bundle install
841
857
  [release-it]: https://pragprog.com/titles/mnee2/release-it-second-edition/
842
858
  [shopify]: http://www.shopify.com/
843
859
  [mysql-semian-adapter]: lib/semian/mysql2.rb
860
+ [postgres-semian-adapter]: https://github.com/mschoenlaub/semian-postgres
844
861
  [redis-semian-adapter]: lib/semian/redis.rb
845
862
  [semian-adapter]: lib/semian/adapter.rb
846
863
  [nethttp-semian-adapter]: lib/semian/net_http.rb
@@ -359,13 +359,13 @@ ms_to_timespec(long ms, struct timespec *ts)
359
359
  ts->tv_nsec = (ms % 1000) * 1000000;
360
360
  }
361
361
 
362
- static inline void
362
+ static void
363
363
  semian_resource_mark(void *ptr)
364
364
  {
365
365
  /* noop */
366
366
  }
367
367
 
368
- static inline void
368
+ static void
369
369
  semian_resource_free(void *ptr)
370
370
  {
371
371
  semian_resource_t *res = (semian_resource_t *) ptr;
@@ -376,7 +376,7 @@ semian_resource_free(void *ptr)
376
376
  xfree(res);
377
377
  }
378
378
 
379
- static inline size_t
379
+ static size_t
380
380
  semian_resource_memsize(const void *ptr)
381
381
  {
382
382
  return sizeof(semian_resource_t);
@@ -390,5 +390,5 @@ semian_resource_type = {
390
390
  semian_resource_free,
391
391
  semian_resource_memsize
392
392
  },
393
- NULL, NULL, RUBY_TYPED_FREE_IMMEDIATELY
393
+ NULL, NULL, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
394
394
  };
@@ -269,3 +269,19 @@ diff_timespec_ms(struct timespec *end, struct timespec *begin)
269
269
  long begin_ms = (begin->tv_sec * 1e3) + (begin->tv_nsec / 1e6);
270
270
  return end_ms - begin_ms;
271
271
  }
272
+
273
+ #ifdef DEBUG
274
+ VALUE
275
+ print_sem_vals_without_rescue(VALUE v_sem_id)
276
+ {
277
+ int sem_id = NUM2INT(v_sem_id);
278
+ printf("[pid=%d][semian] semaphore values lock: %d, tickets: %d configured: %d, registered workers: %d\n",
279
+ getpid(),
280
+ get_sem_val(sem_id, SI_SEM_LOCK),
281
+ get_sem_val(sem_id, SI_SEM_TICKETS),
282
+ get_sem_val(sem_id, SI_SEM_CONFIGURED_TICKETS),
283
+ get_sem_val(sem_id, SI_SEM_REGISTERED_WORKERS)
284
+ );
285
+ return (VALUE)0;
286
+ }
287
+ #endif
@@ -110,16 +110,14 @@ void *
110
110
  acquire_semaphore_without_gvl(void *p);
111
111
 
112
112
  #ifdef DEBUG
113
+ VALUE
114
+ print_sem_vals_without_rescue(VALUE v_sem_id);
115
+
113
116
  static inline void
114
117
  print_sem_vals(int sem_id)
115
118
  {
116
- printf("[pid=%d][semian] semaphore values lock: %d, tickets: %d configured: %d, registered workers: %d\n",
117
- getpid(),
118
- get_sem_val(sem_id, SI_SEM_LOCK),
119
- get_sem_val(sem_id, SI_SEM_TICKETS),
120
- get_sem_val(sem_id, SI_SEM_CONFIGURED_TICKETS),
121
- get_sem_val(sem_id, SI_SEM_REGISTERED_WORKERS)
122
- );
119
+ int state;
120
+ rb_protect(print_sem_vals_without_rescue, INT2NUM(sem_id), &state);
123
121
  }
124
122
  #endif
125
123
 
@@ -36,8 +36,6 @@ module Semian
36
36
  end
37
37
 
38
38
  def acquire(resource = nil, &block)
39
- return yield if disabled?
40
-
41
39
  transition_to_half_open if transition_to_half_open?
42
40
 
43
41
  raise OpenCircuitError unless request_allowed?
@@ -66,7 +64,7 @@ module Semian
66
64
 
67
65
  def mark_failed(error)
68
66
  push_error(error)
69
- push_time(@errors)
67
+ push_time
70
68
  if closed?
71
69
  transition_to_open if error_threshold_reached?
72
70
  elsif half_open?
@@ -131,19 +129,20 @@ module Semian
131
129
  last_error_time = @errors.last
132
130
  return false unless last_error_time
133
131
 
134
- Time.at(last_error_time) + @error_timeout < Time.now
132
+ last_error_time + @error_timeout < Process.clock_gettime(Process::CLOCK_MONOTONIC)
135
133
  end
136
134
 
137
135
  def push_error(error)
138
136
  @last_error = error
139
137
  end
140
138
 
141
- def push_time(window, time: Time.now)
139
+ def push_time
140
+ time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
142
141
  if error_threshold_timeout_enabled
143
- window.reject! { |err_time| err_time + @error_threshold_timeout < time.to_i }
142
+ @errors.reject! { |err_time| err_time + @error_threshold_timeout < time }
144
143
  end
145
144
 
146
- window << time.to_i
145
+ @errors << time
147
146
  end
148
147
 
149
148
  def log_state_transition(new_state)
@@ -151,7 +150,8 @@ module Semian
151
150
 
152
151
  str = "[#{self.class.name}] State transition from #{@state.value} to #{new_state}."
153
152
  str += " success_count=#{@successes.value} error_count=#{@errors.size}"
154
- str += " success_count_threshold=#{@success_count_threshold} error_count_threshold=#{@error_count_threshold}"
153
+ str += " success_count_threshold=#{@success_count_threshold}"
154
+ str += " error_count_threshold=#{@error_count_threshold}"
155
155
  str += " error_timeout=#{@error_timeout} error_last_at=\"#{@errors.last}\""
156
156
  str += " name=\"#{@name}\""
157
157
  if new_state == :open && @last_error
@@ -165,21 +165,14 @@ module Semian
165
165
  Semian.notify(:state_change, self, nil, nil, state: new_state)
166
166
  end
167
167
 
168
- def disabled?
169
- ENV["SEMIAN_CIRCUIT_BREAKER_DISABLED"] || ENV["SEMIAN_DISABLED"]
170
- end
171
-
172
168
  def maybe_with_half_open_resource_timeout(resource, &block)
173
- result =
174
- if half_open? && @half_open_resource_timeout && resource.respond_to?(:with_resource_timeout)
175
- resource.with_resource_timeout(@half_open_resource_timeout) do
176
- block.call
177
- end
178
- else
169
+ if half_open? && @half_open_resource_timeout && resource.respond_to?(:with_resource_timeout)
170
+ resource.with_resource_timeout(@half_open_resource_timeout) do
179
171
  block.call
180
172
  end
181
-
182
- result
173
+ else
174
+ block.call
175
+ end
183
176
  end
184
177
  end
185
178
  end
data/lib/semian/grpc.rb CHANGED
@@ -117,7 +117,7 @@ module Semian
117
117
  execute = operation.singleton_method(:execute)
118
118
  operation.instance_variable_set(:@semian, self)
119
119
  operation.define_singleton_method(:execute) do
120
- @semian.send(:acquire_semian_resource, **{ adapter: :grpc, scope: scope }) { execute.call }
120
+ @semian.send(:acquire_semian_resource, adapter: :grpc, scope: scope) { execute.call }
121
121
  end
122
122
  end
123
123
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "thread"
4
+
3
5
  class LRUHash
4
6
  # This LRU (Least Recently Used) hash will allow
5
7
  # the cleaning of resources as time goes on.
@@ -41,7 +43,7 @@ class LRUHash
41
43
  #
42
44
  # Arguments:
43
45
  # +max_size+ The maximum size of the table
44
- # +min_time+ The minimum time a resource can live in the cache
46
+ # +min_time+ The minimum time in seconds a resource can live in the cache
45
47
  #
46
48
  # Note:
47
49
  # The +min_time+ is a stronger guarantee than +max_size+. That is, if there are
@@ -57,9 +59,9 @@ class LRUHash
57
59
  @table = {}
58
60
  @lock =
59
61
  if Semian.thread_safe?
60
- Mutex.new
62
+ ::Thread::Mutex.new
61
63
  else
62
- NoopMutex.new
64
+ ::LRUHash::NoopMutex.new
63
65
  end
64
66
  end
65
67
 
@@ -83,7 +85,7 @@ class LRUHash
83
85
  @lock.synchronize do
84
86
  @table.delete(key)
85
87
  @table[key] = resource
86
- resource.updated_at = Time.now
88
+ resource.updated_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
87
89
  end
88
90
  clear_unused_resources if @table.length > @max_size
89
91
  end
@@ -98,7 +100,7 @@ class LRUHash
98
100
  found = @table.delete(key)
99
101
  if found
100
102
  @table[key] = found
101
- found.updated_at = Time.now
103
+ found.updated_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
102
104
  end
103
105
  found
104
106
  end
@@ -130,9 +132,10 @@ class LRUHash
130
132
  timer_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
131
133
 
132
134
  ran = try_synchronize do
133
- # Clears resources that have not been used in the last 5 minutes.
135
+ # Clears resources that have not been used
136
+ # in the last 5 minutes (default value of Semian.minimum_lru_time).
134
137
 
135
- stop_time = Time.now - @min_time # Don't process resources updated after this time
138
+ stop_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @min_time
136
139
  @table.each do |_, resource|
137
140
  payload[:examined] += 1
138
141
 
@@ -13,6 +13,6 @@ module Semian
13
13
  end
14
14
 
15
15
  def disabled?
16
- ENV["SEMIAN_SEMAPHORES_DISABLED"] || ENV["SEMIAN_DISABLED"]
16
+ ENV.key?("SEMIAN_SEMAPHORES_DISABLED") || ENV.key?("SEMIAN_DISABLED")
17
17
  end
18
18
  end
@@ -5,8 +5,14 @@ module Semian
5
5
  extend Forwardable
6
6
 
7
7
  def_delegators :@bulkhead, :destroy, :count, :semid, :tickets, :registered_workers
8
- def_delegators :@circuit_breaker, :reset, :mark_failed, :mark_success, :request_allowed?,
9
- :open?, :closed?, :half_open?
8
+ def_delegators :@circuit_breaker,
9
+ :reset,
10
+ :mark_failed,
11
+ :mark_success,
12
+ :request_allowed?,
13
+ :open?,
14
+ :closed?,
15
+ :half_open?
10
16
 
11
17
  attr_reader :bulkhead, :circuit_breaker, :name
12
18
  attr_accessor :updated_at
@@ -15,7 +21,7 @@ module Semian
15
21
  @name = name
16
22
  @bulkhead = bulkhead
17
23
  @circuit_breaker = circuit_breaker
18
- @updated_at = Time.now
24
+ @updated_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
19
25
  end
20
26
 
21
27
  def destroy
@@ -9,7 +9,7 @@ module Semian
9
9
 
10
10
  def initialize(name)
11
11
  @name = name
12
- @updated_at = Time.now
12
+ @updated_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
13
13
  end
14
14
 
15
15
  def registered_workers
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Semian
4
- VERSION = "0.16.0"
4
+ VERSION = "0.17.0"
5
5
  end
data/lib/semian.rb CHANGED
@@ -104,7 +104,7 @@ module Semian
104
104
  attr_accessor :maximum_lru_size, :minimum_lru_time, :default_permissions, :namespace
105
105
 
106
106
  self.maximum_lru_size = 500
107
- self.minimum_lru_time = 300
107
+ self.minimum_lru_time = 300 # 300 seconds / 5 minutes
108
108
  self.default_permissions = 0660
109
109
 
110
110
  def issue_disabled_semaphores_warning
@@ -122,11 +122,16 @@ module Semian
122
122
  attr_accessor :semian_identifier
123
123
 
124
124
  def to_s
125
+ message = super
125
126
  if @semian_identifier
126
- "[#{@semian_identifier}] #{super}"
127
- else
128
- super
127
+ prefix = "[#{@semian_identifier}] "
128
+ # When an error is created from another error's message it might
129
+ # already have a semian identifier in their message
130
+ unless message.start_with?(prefix)
131
+ message = "#{prefix}#{message}"
132
+ end
129
133
  end
134
+ message
130
135
  end
131
136
  end
132
137
 
@@ -156,11 +161,17 @@ module Semian
156
161
  #
157
162
  # +timeout+: Default timeout in seconds. Default 0. (bulkhead)
158
163
  #
164
+ # +error_timeout+: The duration in seconds since the last error after which the error count is reset to 0.
165
+ # (circuit breaker required)
166
+ #
159
167
  # +error_threshold+: The amount of errors that must happen within error_timeout amount of time to open
160
168
  # the circuit. (circuit breaker required)
161
169
  #
162
- # +error_timeout+: The duration in seconds since the last error after which the error count is reset to 0.
163
- # (circuit breaker required)
170
+ # +error_threshold_timeout+: The duration in seconds to examine number of errors to compare with error_threshold.
171
+ # Default same as error_timeout. (circuit breaker)
172
+ #
173
+ # +error_threshold_timeout_enabled+: flag to enable/disable filter time window based error eviction
174
+ # (error_threshold_timeout). Default true. (circuit breaker)
164
175
  #
165
176
  # +success_threshold+: The number of consecutive success after which an half-open circuit will be fully closed.
166
177
  # (circuit breaker required)
@@ -170,6 +181,8 @@ module Semian
170
181
  #
171
182
  # Returns the registered resource.
172
183
  def register(name, **options)
184
+ return UnprotectedResource.new(name) if ENV.key?("SEMIAN_DISABLED")
185
+
173
186
  circuit_breaker = create_circuit_breaker(name, **options)
174
187
  bulkhead = create_bulkhead(name, **options)
175
188
 
@@ -264,8 +277,8 @@ module Semian
264
277
  private
265
278
 
266
279
  def create_circuit_breaker(name, **options)
267
- circuit_breaker = options.fetch(:circuit_breaker, true)
268
- return unless circuit_breaker
280
+ return if ENV.key?("SEMIAN_CIRCUIT_BREAKER_DISABLED")
281
+ return unless options.fetch(:circuit_breaker, true)
269
282
 
270
283
  require_keys!([:success_threshold, :error_threshold, :error_timeout], options)
271
284
 
@@ -304,16 +317,18 @@ module Semian
304
317
  end
305
318
 
306
319
  def create_bulkhead(name, **options)
307
- bulkhead = options.fetch(:bulkhead, true)
308
- return unless bulkhead
320
+ return if ENV.key?("SEMIAN_BULKHEAD_DISABLED")
321
+ return unless options.fetch(:bulkhead, true)
309
322
 
310
323
  permissions = options[:permissions] || default_permissions
311
324
  timeout = options[:timeout] || 0
312
- ::Semian::Resource.new(name,
325
+ ::Semian::Resource.new(
326
+ name,
313
327
  tickets: options[:tickets],
314
328
  quota: options[:quota],
315
329
  permissions: permissions,
316
- timeout: timeout)
330
+ timeout: timeout,
331
+ )
317
332
  end
318
333
 
319
334
  def require_keys!(required, options)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: semian
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Francis
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2022-10-05 00:00:00.000000000 Z
13
+ date: 2023-02-08 00:00:00.000000000 Z
14
14
  dependencies: []
15
15
  description: |2
16
16
  A Ruby C extention that is used to control access to shared resources