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 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: []