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,73 @@
1
+ module Semian
2
+ # This class acts as a replacement for `ProtectedResource` when
3
+ # the semian configuration of an `Adapter` is missing or explicitly disabled
4
+ class UnprotectedResource
5
+ attr_reader :name
6
+ attr_accessor :updated_at
7
+
8
+ def initialize(name)
9
+ @name = name
10
+ @updated_at = Time.now
11
+ end
12
+
13
+ def registered_workers
14
+ 0
15
+ end
16
+
17
+ def tickets
18
+ -1
19
+ end
20
+
21
+ def destroy
22
+ end
23
+
24
+ def acquire(*)
25
+ yield self
26
+ end
27
+
28
+ def count
29
+ 0
30
+ end
31
+
32
+ def semid
33
+ 0
34
+ end
35
+
36
+ def reset
37
+ end
38
+
39
+ def open?
40
+ false
41
+ end
42
+
43
+ def closed?
44
+ true
45
+ end
46
+
47
+ def half_open?
48
+ false
49
+ end
50
+
51
+ def request_allowed?
52
+ true
53
+ end
54
+
55
+ def mark_failed(_error)
56
+ end
57
+
58
+ def mark_success
59
+ end
60
+
61
+ def bulkhead
62
+ nil
63
+ end
64
+
65
+ def circuit_breaker
66
+ nil
67
+ end
68
+
69
+ def in_use?
70
+ true
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,3 @@
1
+ module Semian
2
+ VERSION = '0.11.4.1'
3
+ end
data/lib/semian.rb ADDED
@@ -0,0 +1,310 @@
1
+ require 'forwardable'
2
+ require 'logger'
3
+ require 'weakref'
4
+ require 'thread'
5
+
6
+ require 'semian/version'
7
+ require 'semian/instrumentable'
8
+ require 'semian/platform'
9
+ require 'semian/resource'
10
+ require 'semian/circuit_breaker'
11
+ require 'semian/protected_resource'
12
+ require 'semian/unprotected_resource'
13
+ require 'semian/simple_sliding_window'
14
+ require 'semian/simple_integer'
15
+ require 'semian/simple_state'
16
+ require 'semian/lru_hash'
17
+
18
+ #
19
+ # === Overview
20
+ #
21
+ # Semian is a library that can be used to control access to external services.
22
+ #
23
+ # It's desirable to control access to external services so that in the case that one
24
+ # is slow or not responding, the performance of an entire system is not compromised.
25
+ #
26
+ # Semian uses the concept of a "resource" as an identifier that controls access
27
+ # to some external service. So for example, "mysql" or "redis" would be considered
28
+ # resources. If a system is sharded, like a database, you would typically create
29
+ # a resource for every shard.
30
+ #
31
+ # Resources are visible across an IPC namespace. This means that you can register a
32
+ # resource in one process and access it from another. This is useful in application
33
+ # servers like Unicorn that are multi-process. A resource is persistent. It will
34
+ # continue to exist even after the application exits, and will only be destroyed by
35
+ # manually removing it with the <code>ipcrm</code> command, calling Resource.destroy,
36
+ # or rebooting the machine.
37
+ #
38
+ # Each resource has a configurable number of tickets. Tickets are what controls
39
+ # access to the external service. If a client does not have a ticket, it cannot access
40
+ # a service. If there are no tickets available, the client will block for a configurable
41
+ # amount of time until a ticket is available. If there are no tickets available after
42
+ # the timeout period has elapsed, the client will be unable to access the service and
43
+ # an error will be raised.
44
+ #
45
+ # Resources also integrate a circuit breaker in order to fail faster and to let the
46
+ # resource the time to recover. If `error_threshold` errors happen in the span of `error_timeout`
47
+ # then the circuit will be opened and every attempt to acquire the resource will immediately fail.
48
+ #
49
+ # Once in open state, after `error_timeout` is elapsed, the circuit will transition in the half-open state.
50
+ # In that state a single error will fully re-open the circuit, and the circuit will transition back to the closed
51
+ # state only after the resource is acquired `success_threshold` consecutive times.
52
+ #
53
+ # A resource is registered by using the Semian.register method.
54
+ #
55
+ # ==== Examples
56
+ #
57
+ # ===== Registering a resource
58
+ #
59
+ # Semian.register(:mysql_shard0, tickets: 10, timeout: 0.5, error_threshold: 3, error_timeout: 10, success_threshold: 2)
60
+ #
61
+ # This registers a new resource called <code>:mysql_shard0</code> that has 10 tickets and a default timeout of 500 milliseconds.
62
+ #
63
+ # After 3 failures in the span of 10 seconds the circuit will be open.
64
+ # After an additional 10 seconds it will transition to half-open.
65
+ # And finally after 2 successful acquisitions of the resource it will transition back to the closed state.
66
+ #
67
+ # ===== Using a resource
68
+ #
69
+ # Semian[:mysql_shard0].acquire do
70
+ # # Perform a MySQL query here
71
+ # end
72
+ #
73
+ # This acquires a ticket for the <code>:mysql_shard0</code> resource. If we use the example above, the ticket count would
74
+ # be lowered to 9 when block is executed, then raised to 10 when the block completes.
75
+ #
76
+ # ===== Overriding the default timeout
77
+ #
78
+ # Semian[:mysql_shard0].acquire(timeout: 1) do
79
+ # # Perform a MySQL query here
80
+ # end
81
+ #
82
+ # This is the same as the previous example, but overrides the timeout from the default value of 500 milliseconds to 1 second.
83
+ module Semian
84
+ extend self
85
+ extend Instrumentable
86
+
87
+ BaseError = Class.new(StandardError)
88
+ SyscallError = Class.new(BaseError)
89
+ TimeoutError = Class.new(BaseError)
90
+ InternalError = Class.new(BaseError)
91
+ OpenCircuitError = Class.new(BaseError)
92
+
93
+ attr_accessor :maximum_lru_size, :minimum_lru_time, :default_permissions, :namespace
94
+ self.maximum_lru_size = 500
95
+ self.minimum_lru_time = 300
96
+ self.default_permissions = 0660
97
+
98
+ def issue_disabled_semaphores_warning
99
+ return if defined?(@warning_issued)
100
+ @warning_issued = true
101
+ if !sysv_semaphores_supported?
102
+ logger.info("Semian sysv semaphores are not supported on #{RUBY_PLATFORM} - all operations will no-op")
103
+ elsif disabled?
104
+ logger.info("Semian semaphores are disabled, is this what you really want? - all operations will no-op")
105
+ end
106
+ end
107
+
108
+ module AdapterError
109
+ attr_accessor :semian_identifier
110
+
111
+ def to_s
112
+ if @semian_identifier
113
+ "[#{@semian_identifier}] #{super}"
114
+ else
115
+ super
116
+ end
117
+ end
118
+ end
119
+
120
+ attr_accessor :logger
121
+
122
+ self.logger = Logger.new(STDERR)
123
+
124
+ # Registers a resource.
125
+ #
126
+ # +name+: Name of the resource - this can be either a string or symbol. (required)
127
+ #
128
+ # +circuit_breaker+: The boolean if you want a circuit breaker acquired for your resource. Default true.
129
+ #
130
+ # +bulkhead+: The boolean if you want a bulkhead to be acquired for your resource. Default true.
131
+ #
132
+ # +tickets+: Number of tickets. If this value is 0, the ticket count will not be set,
133
+ # but the resource must have been previously registered otherwise an error will be raised.
134
+ # Mutually exclusive with the 'quota' argument.
135
+ #
136
+ # +quota+: Calculate tickets as a ratio of the number of registered workers.
137
+ # Must be greater than 0, less than or equal to 1. There will always be at least 1 ticket, as it
138
+ # is calculated as (workers * quota).ceil
139
+ # Mutually exclusive with the 'ticket' argument.
140
+ # but the resource must have been previously registered otherwise an error will be raised. (bulkhead)
141
+ #
142
+ # +permissions+: Octal permissions of the resource. Default to +Semian.default_permissions+ (0660). (bulkhead)
143
+ #
144
+ # +timeout+: Default timeout in seconds. Default 0. (bulkhead)
145
+ #
146
+ # +error_threshold+: The amount of errors that must happen within error_timeout amount of time to open
147
+ # the circuit. (circuit breaker required)
148
+ #
149
+ # +error_timeout+: The duration in seconds since the last error after which the error count is reset to 0.
150
+ # (circuit breaker required)
151
+ #
152
+ # +success_threshold+: The number of consecutive success after which an half-open circuit will be fully closed.
153
+ # (circuit breaker required)
154
+ #
155
+ # +exceptions+: An array of exception classes that should be accounted as resource errors. Default [].
156
+ # (circuit breaker)
157
+ #
158
+ # Returns the registered resource.
159
+ def register(name, **options)
160
+ circuit_breaker = create_circuit_breaker(name, **options)
161
+ bulkhead = create_bulkhead(name, **options)
162
+
163
+ if circuit_breaker.nil? && bulkhead.nil?
164
+ raise ArgumentError, 'Both bulkhead and circuitbreaker cannot be disabled.'
165
+ end
166
+
167
+ resources[name] = ProtectedResource.new(name, bulkhead, circuit_breaker)
168
+ end
169
+
170
+ def retrieve_or_register(name, **args)
171
+ # If consumer who retrieved / registered by a Semian::Adapter, keep track
172
+ # of who the consumer was so that we can clear the resource reference if needed.
173
+ if consumer = args.delete(:consumer)
174
+ if consumer.class.include?(Semian::Adapter)
175
+ consumers[name] ||= []
176
+ consumers[name] << WeakRef.new(consumer)
177
+ end
178
+ end
179
+ self[name] || register(name, **args)
180
+ end
181
+
182
+ # Retrieves a resource by name.
183
+ def [](name)
184
+ resources[name]
185
+ end
186
+
187
+ def destroy(name)
188
+ if resource = resources.delete(name)
189
+ resource.destroy
190
+ end
191
+ end
192
+
193
+ # Unregister will not destroy the semian resource, but it will
194
+ # remove it from the hash of registered resources, and decrease
195
+ # the number of registered workers.
196
+ # Semian.destroy removes the underlying resource, but
197
+ # Semian.unregister will remove all references, while preserving
198
+ # the underlying semian resource (and sysV semaphore).
199
+ # Also clears any semian_resources
200
+ # in use by any semian adapters if the weak reference is still alive.
201
+ def unregister(name)
202
+ if resource = resources.delete(name)
203
+ resource.bulkhead.unregister_worker if resource.bulkhead
204
+ consumers_for_resource = consumers.delete(name) || []
205
+ consumers_for_resource.each do |consumer|
206
+ begin
207
+ if consumer.weakref_alive?
208
+ consumer.clear_semian_resource
209
+ end
210
+ rescue WeakRef::RefError
211
+ next
212
+ end
213
+ end
214
+ end
215
+ end
216
+
217
+ # Unregisters all resources
218
+ def unregister_all_resources
219
+ resources.keys.each do |resource|
220
+ unregister(resource)
221
+ end
222
+ end
223
+
224
+ # Retrieves a hash of all registered resources.
225
+ def resources
226
+ @resources ||= LRUHash.new
227
+ end
228
+
229
+ # Retrieves a hash of all registered resource consumers.
230
+ def consumers
231
+ @consumers ||= {}
232
+ end
233
+
234
+ def reset!
235
+ @consumers = {}
236
+ @resources = LRUHash.new
237
+ end
238
+
239
+ def thread_safe?
240
+ return @thread_safe if defined?(@thread_safe)
241
+ @thread_safe = true
242
+ end
243
+
244
+ def thread_safe=(thread_safe)
245
+ @thread_safe = thread_safe
246
+ end
247
+
248
+ private
249
+
250
+ def create_circuit_breaker(name, **options)
251
+ circuit_breaker = options.fetch(:circuit_breaker, true)
252
+ return unless circuit_breaker
253
+ require_keys!([:success_threshold, :error_threshold, :error_timeout], options)
254
+
255
+ exceptions = options[:exceptions] || []
256
+ CircuitBreaker.new(
257
+ name,
258
+ success_threshold: options[:success_threshold],
259
+ error_threshold: options[:error_threshold],
260
+ error_timeout: options[:error_timeout],
261
+ exceptions: Array(exceptions) + [::Semian::BaseError],
262
+ half_open_resource_timeout: options[:half_open_resource_timeout],
263
+ implementation: implementation(**options),
264
+ )
265
+ end
266
+
267
+ def implementation(**options)
268
+ # thread_safety_disabled will be replaced by a global setting
269
+ # Semian is thread safe by default. It is possible
270
+ # to modify the value by using Semian.thread_safe=
271
+ unless options[:thread_safety_disabled].nil?
272
+ logger.info(
273
+ "NOTE: thread_safety_disabled will be replaced by a global setting" \
274
+ "Semian is thread safe by default. It is possible" \
275
+ "to modify the value by using Semian.thread_safe=",
276
+ )
277
+ end
278
+
279
+ thread_safe = options[:thread_safety_disabled].nil? ? Semian.thread_safe? : !options[:thread_safety_disabled]
280
+ thread_safe ? ::Semian::ThreadSafe : ::Semian::Simple
281
+ end
282
+
283
+ def create_bulkhead(name, **options)
284
+ bulkhead = options.fetch(:bulkhead, true)
285
+ return unless bulkhead
286
+
287
+ permissions = options[:permissions] || default_permissions
288
+ timeout = options[:timeout] || 0
289
+ Resource.new(name, tickets: options[:tickets], quota: options[:quota], permissions: permissions, timeout: timeout)
290
+ end
291
+
292
+ def require_keys!(required, options)
293
+ diff = required - options.keys
294
+ unless diff.empty?
295
+ raise ArgumentError, "Missing required arguments for Semian: #{diff}"
296
+ end
297
+ end
298
+ end
299
+
300
+ if Semian.semaphores_enabled?
301
+ require 'semian/semian'
302
+ else
303
+ Semian::MAX_TICKETS = 0
304
+ end
305
+
306
+ if defined? ActiveSupport
307
+ ActiveSupport.on_load :active_record do
308
+ require 'semian/rails'
309
+ end
310
+ end
metadata ADDED
@@ -0,0 +1,260 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: semian_extension
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.11.4.1
5
+ platform: ruby
6
+ authors:
7
+ - Scott Francis
8
+ - Simon Eskildsen
9
+ - Dale Hamel
10
+ - Sajan Gupta
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2021-11-01 00:00:00.000000000 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: rake-compiler
18
+ requirement: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: '0'
37
+ type: :development
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ - !ruby/object:Gem::Dependency
45
+ name: timecop
46
+ requirement: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ type: :development
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ - !ruby/object:Gem::Dependency
59
+ name: minitest
60
+ requirement: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ type: :development
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ - !ruby/object:Gem::Dependency
73
+ name: pry-byebug
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ type: :development
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ - !ruby/object:Gem::Dependency
87
+ name: mysql2
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ type: :development
94
+ prerelease: false
95
+ version_requirements: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ - !ruby/object:Gem::Dependency
101
+ name: redis
102
+ requirement: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ type: :development
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ - !ruby/object:Gem::Dependency
115
+ name: webrick
116
+ requirement: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ type: :development
122
+ prerelease: false
123
+ version_requirements: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ - !ruby/object:Gem::Dependency
129
+ name: toxiproxy
130
+ requirement: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - "~>"
133
+ - !ruby/object:Gem::Version
134
+ version: 1.0.0
135
+ type: :development
136
+ prerelease: false
137
+ version_requirements: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - "~>"
140
+ - !ruby/object:Gem::Version
141
+ version: 1.0.0
142
+ - !ruby/object:Gem::Dependency
143
+ name: grpc
144
+ requirement: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ type: :development
150
+ prerelease: false
151
+ version_requirements: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ - !ruby/object:Gem::Dependency
157
+ name: mocha
158
+ requirement: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
163
+ type: :development
164
+ prerelease: false
165
+ version_requirements: !ruby/object:Gem::Requirement
166
+ requirements:
167
+ - - ">="
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
170
+ - !ruby/object:Gem::Dependency
171
+ name: memory_profiler
172
+ requirement: !ruby/object:Gem::Requirement
173
+ requirements:
174
+ - - ">="
175
+ - !ruby/object:Gem::Version
176
+ version: '0'
177
+ type: :development
178
+ prerelease: false
179
+ version_requirements: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - ">="
182
+ - !ruby/object:Gem::Version
183
+ version: '0'
184
+ - !ruby/object:Gem::Dependency
185
+ name: benchmark-memory
186
+ requirement: !ruby/object:Gem::Requirement
187
+ requirements:
188
+ - - ">="
189
+ - !ruby/object:Gem::Version
190
+ version: '0'
191
+ type: :development
192
+ prerelease: false
193
+ version_requirements: !ruby/object:Gem::Requirement
194
+ requirements:
195
+ - - ">="
196
+ - !ruby/object:Gem::Version
197
+ version: '0'
198
+ description: |2
199
+ A Ruby C extention that is used to control access to shared resources
200
+ across process boundaries with SysV semaphores.
201
+ email: root@1mg.com
202
+ executables: []
203
+ extensions:
204
+ - ext/semian/extconf.rb
205
+ extra_rdoc_files: []
206
+ files:
207
+ - ext/semian/extconf.rb
208
+ - ext/semian/resource.c
209
+ - ext/semian/resource.h
210
+ - ext/semian/semian.c
211
+ - ext/semian/semian.h
212
+ - ext/semian/sysv_semaphores.c
213
+ - ext/semian/sysv_semaphores.h
214
+ - ext/semian/tickets.c
215
+ - ext/semian/tickets.h
216
+ - ext/semian/types.h
217
+ - lib/semian.rb
218
+ - lib/semian/adapter.rb
219
+ - lib/semian/circuit_breaker.rb
220
+ - lib/semian/grpc.rb
221
+ - lib/semian/instrumentable.rb
222
+ - lib/semian/lru_hash.rb
223
+ - lib/semian/mysql2.rb
224
+ - lib/semian/net_http.rb
225
+ - lib/semian/platform.rb
226
+ - lib/semian/protected_resource.rb
227
+ - lib/semian/rails.rb
228
+ - lib/semian/redis.rb
229
+ - lib/semian/resource.rb
230
+ - lib/semian/simple_integer.rb
231
+ - lib/semian/simple_sliding_window.rb
232
+ - lib/semian/simple_state.rb
233
+ - lib/semian/typhoeus.rb
234
+ - lib/semian/unprotected_resource.rb
235
+ - lib/semian/version.rb
236
+ homepage: https://github.com/1mgOfficial/semian
237
+ licenses:
238
+ - MIT
239
+ metadata:
240
+ allowed_push_host: https://rubygems.org
241
+ post_install_message:
242
+ rdoc_options: []
243
+ require_paths:
244
+ - lib
245
+ required_ruby_version: !ruby/object:Gem::Requirement
246
+ requirements:
247
+ - - ">="
248
+ - !ruby/object:Gem::Version
249
+ version: '0'
250
+ required_rubygems_version: !ruby/object:Gem::Requirement
251
+ requirements:
252
+ - - ">="
253
+ - !ruby/object:Gem::Version
254
+ version: '0'
255
+ requirements: []
256
+ rubygems_version: 3.0.3
257
+ signing_key:
258
+ specification_version: 4
259
+ summary: Bulkheading for Ruby with SysV semaphores
260
+ test_files: []