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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 04219b7eea123cad23d49905ec52c376484013fb
4
- data.tar.gz: 4922f4c4f148425ebb7d1ced40dc8bf6fc3454ec
3
+ metadata.gz: 2ea7b3c7c6d5ba837117b012f0acd14d061a055f
4
+ data.tar.gz: bfd642ac2f32a39912d4d0696e1454ae218b2484
5
5
  SHA512:
6
- metadata.gz: 896373f2c1401134888a8ac3d503663bcbec528f7d304173196310ec7a585f3e5c01faa0c92ce199ba935b8324e4fa57a1f177f77503bfcd59219e64f576952e
7
- data.tar.gz: ea74197c538ba50e31c01a518570f3bb94f60d5f28be77554616dc9c2595cbf4be24e280cff612d66ef0c970737f025971f93316c2c6a9a3d9a83e1472698e20
6
+ metadata.gz: 3339e53e0c0b4170b27194366c8b1f1b8fb3f0a24483ed5b886b5ea9886024dfeb8c242391ca23f68959605d46c94e67d463741616afc25b05740abe32b4be7e
7
+ data.tar.gz: 27db27c22c66ab98908bf84efb1fcee3809f17280cd61a01eb2e547a5eaf1430c7124b598dc7a53c25664c9cf090ae9712ae8a884247673ee8f4e63c2f01a88b
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- semian (0.0.4)
4
+ semian (0.0.5)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1,31 +1,83 @@
1
- ## Semian
1
+ ## Semian [![Build Status](https://travis-ci.org/Shopify/semian.svg?branch=master)](https://travis-ci.org/Shopify/semian)
2
2
 
3
- [![Build Status](https://travis-ci.org/csfrancis/semian.svg?branch=master)](https://travis-ci.org/csfrancis/semian)
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
- Inspired by the bulkhead resource isolation pattern used in [Hystrix](https://github.com/Netflix/Hystrix/wiki/How-it-Works#Isolation), Semian aims to provide a Ruby API that can be used to control access to external resources.
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
- This can be used with a forking Ruby application server like Unicorn to prevent app server starvation when a resource is slow or not responding.
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 (number of concurrent clients):
12
- ```ruby
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
- # Query mysql and do things
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 does not fork, you can still use the same namespace to control access to a shared resource:
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
- # Query mysql and do things
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
- require 'rake/extensiontask'
18
- Rake::ExtensionTask.new('semian', GEMSPEC) do |ext|
19
- ext.ext_dir = 'ext/semian'
20
- ext.lib_dir = 'lib/semian'
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 = FileList['test/test_*.rb']
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 system_max_sempahore_count;
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
- set_sempahore_permissions(int sem_id, int permissions)
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
- sempahore structure yet - wait a bit in that case */
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) > system_max_sempahore_count) {
292
- rb_raise(rb_eArgError, "ticket count must be a non-negative value and less than %d", system_max_sempahore_count);
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
- set_sempahore_permissions(res->sem_id, FIX2LONG(permissions));
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
- acquire_sempahore_without_gvl(void *p)
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(acquire_sempahore_without_gvl, &res, RUBY_UBF_IO, NULL);
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 sempahore count - semctl() returned %d: %s ", errno, strerror(errno));
507
+ rb_raise(eInternal, "unable to determine maximum semaphore count - semctl() returned %d: %s ", errno, strerror(errno));
508
508
  }
509
- system_max_sempahore_count = info_buf.semvmx;
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(system_max_sempahore_count));
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: 0600, timeout: 1)
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,6 @@
1
+ class Semian
2
+ # Determines if Semian supported on the current platform.
3
+ def self.supported_platform?
4
+ RUBY_PLATFORM.end_with?('-linux')
5
+ end
6
+ end
@@ -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
@@ -1,3 +1,3 @@
1
1
  class Semian
2
- VERSION = '0.0.4'
2
+ VERSION = '0.0.5'
3
3
  end
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 = 'SysV semaphore based library for shared resource control'
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
 
@@ -0,0 +1,11 @@
1
+ require 'test/unit'
2
+ require 'semian'
3
+
4
+ class TestSemian < Test::Unit::TestCase
5
+ def test_unsupported_acquire_yields
6
+ acquired = false
7
+ Semian.register :testing, tickets: 1
8
+ Semian[:testing].acquire { acquired = true }
9
+ assert acquired
10
+ end
11
+ end
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
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-10-05 00:00:00.000000000 Z
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: SysV semaphore based library for shared resource control
76
+ summary: Bulkheading for Ruby with SysV semaphores
73
77
  test_files: []