semian 0.0.4 → 0.0.5
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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +62 -10
- data/Rakefile +16 -6
- data/ext/semian/semian.c +11 -11
- data/lib/semian.rb +8 -3
- data/lib/semian/platform.rb +6 -0
- data/lib/semian/unsupported.rb +21 -0
- data/lib/semian/version.rb +1 -1
- data/semian.gemspec +3 -3
- data/test/test_unsupported.rb +11 -0
- metadata +8 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2ea7b3c7c6d5ba837117b012f0acd14d061a055f
|
4
|
+
data.tar.gz: bfd642ac2f32a39912d4d0696e1454ae218b2484
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3339e53e0c0b4170b27194366c8b1f1b8fb3f0a24483ed5b886b5ea9886024dfeb8c242391ca23f68959605d46c94e67d463741616afc25b05740abe32b4be7e
|
7
|
+
data.tar.gz: 27db27c22c66ab98908bf84efb1fcee3809f17280cd61a01eb2e547a5eaf1430c7124b598dc7a53c25664c9cf090ae9712ae8a884247673ee8f4e63c2f01a88b
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,31 +1,83 @@
|
|
1
|
-
## Semian
|
1
|
+
## Semian [](https://travis-ci.org/Shopify/semian)
|
2
2
|
|
3
|
-
|
3
|
+
Semian is a Ruby implementation of the Bulkhead resource isolation pattern,
|
4
|
+
using SysV semaphores. Bulkheading controls access to external resources,
|
5
|
+
protecting against resource or network latency, by allowing otherwise slow
|
6
|
+
queries to fail fast.
|
4
7
|
|
5
|
-
|
8
|
+
Downtime is easy to detect. Requests fail when querying the resource, usually
|
9
|
+
fast. Reliably detecting higher than normal latency is more difficult. Strict
|
10
|
+
timeouts is one solution, but picking those are hard and usually needs to be
|
11
|
+
done per query or section of your application.
|
6
12
|
|
7
|
-
|
13
|
+
Semian takes a different approach. Instead of asking the question: "How long can
|
14
|
+
my query execute?" it raises the question "How long do I want to wait before
|
15
|
+
starting to execute my query?".
|
16
|
+
|
17
|
+
Imagine that your database is very slow. Requests that hit the slow database are
|
18
|
+
processed in your workers and end up timing out at the worker level. However,
|
19
|
+
other requests don't touch the slow database. These requests will start to queue
|
20
|
+
up behind the requests to the slow database, possibly never being served
|
21
|
+
because the client disconnects due to slowness. You're now effectively down,
|
22
|
+
because a single external resource is slow.
|
23
|
+
|
24
|
+
Semian solves this problem with resource tickets. Every time a worker addresses
|
25
|
+
an external resource, it takes a ticket for the duration of the query. When the
|
26
|
+
query returns, it puts the ticket back into the pool. If you have `n` tickets,
|
27
|
+
and the `n + 1` worker tries to acquire a ticket to query the resource it'll
|
28
|
+
wait for `timeout` seconds to see if a ticket comes available, otherwise it'll
|
29
|
+
raise `Semian::TimeoutError`.
|
30
|
+
|
31
|
+
By failing fast, this solves the problem of one slow database taking your
|
32
|
+
platform down. The busyness of the external resource determines the `timeout`
|
33
|
+
and ticket count. You can also rescue `Semian::TimeoutError` to provide fallback
|
34
|
+
values, such as showing an error message to the user.
|
35
|
+
|
36
|
+
A subset of workers will still be tied up on the slow database, meaning you are
|
37
|
+
under capacity with a slow external resource. However, at most you'll have
|
38
|
+
`ticket count` workers occupied. This is a small price to pay. By implementing
|
39
|
+
the circuit breaker pattern on top of Semian, you can avoid that. That may be
|
40
|
+
built into Semian in the future.
|
41
|
+
|
42
|
+
Under the hood, Semian is implemented with SysV semaphores. In a threaded web
|
43
|
+
server, the semaphore could be in-process. Semian was written with forked web
|
44
|
+
servers in mind, such as Unicorn, but Semian can be used perfectly fine in a
|
45
|
+
threaded web server.
|
8
46
|
|
9
47
|
### Usage
|
10
48
|
|
11
|
-
In a master process, register a resource with a specified number of tickets
|
12
|
-
|
13
|
-
require 'semian'
|
49
|
+
In a master process, register a resource with a specified number of tickets
|
50
|
+
(number of concurrent clients):
|
14
51
|
|
52
|
+
```ruby
|
15
53
|
Semian.register(:mysql_master, tickets: 3, timeout: 0.5)
|
16
54
|
```
|
17
55
|
|
18
56
|
Then in your child processes, you can use the resource:
|
57
|
+
|
19
58
|
```ruby
|
20
59
|
Semian[:mysql_master].acquire do
|
21
|
-
|
60
|
+
# Query the database. If three other workers are querying this resource at the
|
61
|
+
# same time, this block will block for up to 0.5s waiting for another worker
|
62
|
+
# to release a ticket. Otherwise, it'll raise `Semian::TimeoutError`.
|
22
63
|
end
|
23
64
|
```
|
24
65
|
|
25
|
-
If you have a process that
|
66
|
+
If you have a process that doesn't `fork`, you can still use the same namespace
|
67
|
+
to control access to a shared resource:
|
68
|
+
|
26
69
|
```ruby
|
27
70
|
Semian.register(:mysql_master, timeout: 0.5)
|
71
|
+
|
28
72
|
Semian[:mysql_master].acquire do
|
29
|
-
|
73
|
+
# Query the resource
|
30
74
|
end
|
31
75
|
```
|
76
|
+
|
77
|
+
### Install
|
78
|
+
|
79
|
+
In your Gemfile:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
gem "semian"
|
83
|
+
```
|
data/Rakefile
CHANGED
@@ -14,12 +14,18 @@ end
|
|
14
14
|
# Ruby Extension
|
15
15
|
# ==========================================================
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
$:.unshift File.expand_path("../lib", __FILE__)
|
18
|
+
require 'semian/platform'
|
19
|
+
if Semian.supported_platform?
|
20
|
+
require 'rake/extensiontask'
|
21
|
+
Rake::ExtensionTask.new('semian', GEMSPEC) do |ext|
|
22
|
+
ext.ext_dir = 'ext/semian'
|
23
|
+
ext.lib_dir = 'lib/semian'
|
24
|
+
end
|
25
|
+
task :build => :compile
|
26
|
+
else
|
27
|
+
task :build do; end
|
21
28
|
end
|
22
|
-
task :build => :compile
|
23
29
|
|
24
30
|
# ==========================================================
|
25
31
|
# Testing
|
@@ -27,7 +33,11 @@ task :build => :compile
|
|
27
33
|
|
28
34
|
require 'rake/testtask'
|
29
35
|
Rake::TestTask.new 'test' do |t|
|
30
|
-
t.test_files =
|
36
|
+
t.test_files = if Semian.supported_platform?
|
37
|
+
FileList['test/test_semian.rb']
|
38
|
+
else
|
39
|
+
FileList['test/test_unsupported.rb']
|
40
|
+
end
|
31
41
|
end
|
32
42
|
task :test => :build
|
33
43
|
|
data/ext/semian/semian.c
CHANGED
@@ -33,7 +33,7 @@ typedef VALUE (*my_blocking_fn_t)(void*);
|
|
33
33
|
|
34
34
|
static ID id_timeout;
|
35
35
|
static VALUE eSyscall, eTimeout, eInternal;
|
36
|
-
static int
|
36
|
+
static int system_max_semaphore_count;
|
37
37
|
|
38
38
|
static const int kIndexTickets = 0;
|
39
39
|
static const int kIndexTicketMax = 1;
|
@@ -116,7 +116,7 @@ semian_resource_alloc(VALUE klass)
|
|
116
116
|
}
|
117
117
|
|
118
118
|
static void
|
119
|
-
|
119
|
+
set_semaphore_permissions(int sem_id, int permissions)
|
120
120
|
{
|
121
121
|
union semun sem_opts;
|
122
122
|
struct semid_ds stat_buf;
|
@@ -202,7 +202,7 @@ configure_tickets(int sem_id, int tickets, int should_initialize)
|
|
202
202
|
}
|
203
203
|
} else if (tickets > 0) {
|
204
204
|
/* it's possible that we haven't actually initialized the
|
205
|
-
|
205
|
+
semaphore structure yet - wait a bit in that case */
|
206
206
|
if (get_max_tickets(sem_id) == 0) {
|
207
207
|
gettimeofday(&start_time, NULL);
|
208
208
|
while (get_max_tickets(sem_id) == 0) {
|
@@ -288,8 +288,8 @@ semian_resource_initialize(VALUE self, VALUE id, VALUE tickets, VALUE permission
|
|
288
288
|
if (TYPE(default_timeout) != T_FIXNUM && TYPE(default_timeout) != T_FLOAT) {
|
289
289
|
rb_raise(rb_eTypeError, "expected numeric type for default_timeout");
|
290
290
|
}
|
291
|
-
if (FIX2LONG(tickets) < 0 || FIX2LONG(tickets) >
|
292
|
-
rb_raise(rb_eArgError, "ticket count must be a non-negative value and less than %d",
|
291
|
+
if (FIX2LONG(tickets) < 0 || FIX2LONG(tickets) > system_max_semaphore_count) {
|
292
|
+
rb_raise(rb_eArgError, "ticket count must be a non-negative value and less than %d", system_max_semaphore_count);
|
293
293
|
}
|
294
294
|
if (NUM2DBL(default_timeout) < 0) {
|
295
295
|
rb_raise(rb_eArgError, "default timeout must be non-negative value");
|
@@ -312,7 +312,7 @@ semian_resource_initialize(VALUE self, VALUE id, VALUE tickets, VALUE permission
|
|
312
312
|
|
313
313
|
configure_tickets(res->sem_id, FIX2LONG(tickets), created);
|
314
314
|
|
315
|
-
|
315
|
+
set_semaphore_permissions(res->sem_id, FIX2LONG(permissions));
|
316
316
|
|
317
317
|
return self;
|
318
318
|
}
|
@@ -329,7 +329,7 @@ cleanup_semian_resource_acquire(VALUE self)
|
|
329
329
|
}
|
330
330
|
|
331
331
|
static void *
|
332
|
-
|
332
|
+
acquire_semaphore_without_gvl(void *p)
|
333
333
|
{
|
334
334
|
semian_resource_t *res = (semian_resource_t *) p;
|
335
335
|
res->error = 0;
|
@@ -377,7 +377,7 @@ semian_resource_acquire(int argc, VALUE *argv, VALUE self)
|
|
377
377
|
}
|
378
378
|
|
379
379
|
/* release the GVL to acquire the semaphore */
|
380
|
-
WITHOUT_GVL(
|
380
|
+
WITHOUT_GVL(acquire_semaphore_without_gvl, &res, RUBY_UBF_IO, NULL);
|
381
381
|
if (res.error != 0) {
|
382
382
|
if (res.error == EAGAIN) {
|
383
383
|
rb_raise(eTimeout, "timed out waiting for resource '%s'", res.name);
|
@@ -504,10 +504,10 @@ void Init_semian()
|
|
504
504
|
id_timeout = rb_intern("timeout");
|
505
505
|
|
506
506
|
if (semctl(0, 0, SEM_INFO, &info_buf) == -1) {
|
507
|
-
rb_raise(eInternal, "unable to determine maximum
|
507
|
+
rb_raise(eInternal, "unable to determine maximum semaphore count - semctl() returned %d: %s ", errno, strerror(errno));
|
508
508
|
}
|
509
|
-
|
509
|
+
system_max_semaphore_count = info_buf.semvmx;
|
510
510
|
|
511
511
|
/* Maximum number of tickets available on this system. */
|
512
|
-
rb_define_const(cSemian, "MAX_TICKETS", INT2FIX(
|
512
|
+
rb_define_const(cSemian, "MAX_TICKETS", INT2FIX(system_max_semaphore_count));
|
513
513
|
}
|
data/lib/semian.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'semian/semian'
|
2
|
-
|
3
1
|
#
|
4
2
|
# === Overview
|
5
3
|
#
|
@@ -68,7 +66,7 @@ class Semian
|
|
68
66
|
# +timeout+: Default timeout in seconds.
|
69
67
|
#
|
70
68
|
# Returns the registered resource.
|
71
|
-
def register(name, tickets: 0, permissions:
|
69
|
+
def register(name, tickets: 0, permissions: 0660, timeout: 1)
|
72
70
|
resource = Resource.new(name, tickets, permissions, timeout)
|
73
71
|
resources[name] = resource
|
74
72
|
end
|
@@ -85,4 +83,11 @@ class Semian
|
|
85
83
|
end
|
86
84
|
end
|
87
85
|
|
86
|
+
require 'semian/platform'
|
87
|
+
if Semian.supported_platform?
|
88
|
+
require 'semian/semian'
|
89
|
+
else
|
90
|
+
require 'semian/unsupported'
|
91
|
+
$stderr.puts "Semian is not supported on #{RUBY_PLATFORM} - all operations will no-op"
|
92
|
+
end
|
88
93
|
require 'semian/version'
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Semian
|
2
|
+
MAX_TICKETS = 0
|
3
|
+
|
4
|
+
class Resource #:nodoc:
|
5
|
+
def initialize(name, tickets, permissions, timeout); end
|
6
|
+
|
7
|
+
def destroy; end
|
8
|
+
|
9
|
+
def acquire
|
10
|
+
yield self
|
11
|
+
end
|
12
|
+
|
13
|
+
def count
|
14
|
+
0
|
15
|
+
end
|
16
|
+
|
17
|
+
def semid
|
18
|
+
0
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/semian/version.rb
CHANGED
data/semian.gemspec
CHANGED
@@ -5,13 +5,13 @@ require 'semian/version'
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = 'semian'
|
7
7
|
s.version = Semian::VERSION
|
8
|
-
s.summary = '
|
8
|
+
s.summary = 'Bulkheading for Ruby with SysV semaphores'
|
9
9
|
s.description = <<-DOC
|
10
10
|
A Ruby C extention that is used to control access to shared resources
|
11
|
-
across process boundaries.
|
11
|
+
across process boundaries with SysV semaphores.
|
12
12
|
DOC
|
13
13
|
s.homepage = 'https://github.com/csfrancis/semian'
|
14
|
-
s.authors = 'Scott Francis'
|
14
|
+
s.authors = ['Scott Francis', 'Simon Eskildsen']
|
15
15
|
s.email = 'scott.francis@shopify.com'
|
16
16
|
s.license = 'MIT'
|
17
17
|
|
metadata
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: semian
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Scott Francis
|
8
|
+
- Simon Eskildsen
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date: 2014-
|
12
|
+
date: 2014-11-03 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: rake-compiler
|
@@ -26,7 +27,7 @@ dependencies:
|
|
26
27
|
version: '0.9'
|
27
28
|
description: |2
|
28
29
|
A Ruby C extention that is used to control access to shared resources
|
29
|
-
across process boundaries.
|
30
|
+
across process boundaries with SysV semaphores.
|
30
31
|
email: scott.francis@shopify.com
|
31
32
|
executables: []
|
32
33
|
extensions:
|
@@ -43,9 +44,12 @@ files:
|
|
43
44
|
- ext/semian/extconf.rb
|
44
45
|
- ext/semian/semian.c
|
45
46
|
- lib/semian.rb
|
47
|
+
- lib/semian/platform.rb
|
48
|
+
- lib/semian/unsupported.rb
|
46
49
|
- lib/semian/version.rb
|
47
50
|
- semian.gemspec
|
48
51
|
- test/test_semian.rb
|
52
|
+
- test/test_unsupported.rb
|
49
53
|
homepage: https://github.com/csfrancis/semian
|
50
54
|
licenses:
|
51
55
|
- MIT
|
@@ -69,5 +73,5 @@ rubyforge_project:
|
|
69
73
|
rubygems_version: 2.2.2
|
70
74
|
signing_key:
|
71
75
|
specification_version: 4
|
72
|
-
summary:
|
76
|
+
summary: Bulkheading for Ruby with SysV semaphores
|
73
77
|
test_files: []
|