semian 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|