skein 0.3.7 → 0.8.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.
Files changed (43) hide show
  1. checksums.yaml +5 -5
  2. data/Gemfile +4 -1
  3. data/Gemfile.lock +49 -36
  4. data/LICENSE.md +1 -1
  5. data/README.md +9 -4
  6. data/RELEASES.md +2 -0
  7. data/VERSION +1 -1
  8. data/bin/skein +1 -1
  9. data/examples/echo +66 -0
  10. data/examples/echo-server +77 -0
  11. data/lib/skein/adapter.rb +3 -0
  12. data/lib/skein/client/publisher.rb +10 -2
  13. data/lib/skein/client/rpc.rb +78 -19
  14. data/lib/skein/client/subscriber.rb +27 -4
  15. data/lib/skein/client/worker.rb +221 -63
  16. data/lib/skein/client.rb +20 -11
  17. data/lib/skein/config.rb +3 -1
  18. data/lib/skein/connected.rb +88 -17
  19. data/lib/skein/context.rb +5 -2
  20. data/lib/skein/handler/async.rb +6 -2
  21. data/lib/skein/handler.rb +131 -20
  22. data/lib/skein/rabbitmq.rb +1 -0
  23. data/lib/skein/timeout_queue.rb +43 -0
  24. data/lib/skein.rb +18 -2
  25. data/skein.gemspec +21 -24
  26. data/test/helper.rb +29 -0
  27. data/test/unit/test_skein_client.rb +4 -1
  28. data/test/unit/test_skein_client_publisher.rb +1 -1
  29. data/test/unit/test_skein_client_rpc.rb +37 -0
  30. data/test/unit/test_skein_client_subscriber.rb +29 -12
  31. data/test/unit/test_skein_client_worker.rb +22 -9
  32. data/test/unit/test_skein_connected.rb +21 -0
  33. data/test/unit/test_skein_rpc_timeout.rb +19 -0
  34. data/test/unit/test_skein_worker.rb +4 -0
  35. metadata +41 -16
  36. data/lib/skein/rpc/base.rb +0 -23
  37. data/lib/skein/rpc/error.rb +0 -34
  38. data/lib/skein/rpc/notification.rb +0 -2
  39. data/lib/skein/rpc/request.rb +0 -62
  40. data/lib/skein/rpc/response.rb +0 -38
  41. data/lib/skein/rpc.rb +0 -24
  42. data/test/unit/test_skein_rpc_error.rb +0 -10
  43. data/test/unit/test_skein_rpc_request.rb +0 -93
data/lib/skein/context.rb CHANGED
@@ -33,12 +33,15 @@ class Skein::Context
33
33
  end
34
34
 
35
35
  def exception!(*args)
36
- @reporter and @reporter.exception!(*args)
36
+ @reporter&.exception!(*args)
37
37
  end
38
38
 
39
39
  def trap
40
40
  yield
41
- rescue => e
41
+
42
+ rescue SystemExit
43
+ raise
44
+ rescue Object => e
42
45
  self.exception!(e)
43
46
  end
44
47
  end
@@ -2,8 +2,12 @@ class Skein::Handler::Async < Skein::Handler
2
2
  # == Instance Methods =====================================================
3
3
 
4
4
  def delegate(*args)
5
- @target.send(*args) do |response, error|
6
- yield(response, error)
5
+ fiber = Fiber.new do
6
+ @target.send(*args) do |*response|
7
+ Fiber.yield(*response)
8
+ end
7
9
  end
10
+
11
+ yield(fiber.resume)
8
12
  end
9
13
  end
data/lib/skein/handler.rb CHANGED
@@ -3,6 +3,12 @@ class Skein::Handler
3
3
 
4
4
  attr_reader :context
5
5
 
6
+ # == Constants ============================================================
7
+
8
+ RPC_BASE = {
9
+ jsonrpc: '2.0'
10
+ }.freeze
11
+
6
12
  # == Class Methods ========================================================
7
13
 
8
14
  def self.for(target)
@@ -18,10 +24,19 @@ class Skein::Handler
18
24
 
19
25
  def initialize(target, context = nil)
20
26
  @target = target
21
- @context = context
27
+ @context = context || target.context
28
+ end
29
+
30
+ def json_rpc(contents)
31
+ JSON.dump(RPC_BASE.merge(contents))
22
32
  end
23
33
 
24
- def handle(message_json)
34
+ def handle(message_json, metrics, state)
35
+ state[:started] = Time.now
36
+ state[:finished] = nil
37
+ state[:thread] = Thread.current
38
+ state[:method] = :handle
39
+
25
40
  request =
26
41
  begin
27
42
  JSON.load(message_json)
@@ -29,9 +44,16 @@ class Skein::Handler
29
44
  rescue Object => e
30
45
  @context and @context.exception!(e, message_json)
31
46
 
32
- return yield(JSON.dump(
33
- result: nil,
34
- error: '[%s] %s' % [ e.class, e ],
47
+ metrics[:failed] += 1
48
+ metrics[:errors][:parsing] += 1
49
+ metrics[:time] += state[:finished] - state[:started]
50
+ state[:method] = nil
51
+
52
+ return yield(rpc_json(
53
+ error: {
54
+ code: -32700,
55
+ message: 'Parse error'
56
+ },
35
57
  id: nil
36
58
  ))
37
59
  end
@@ -40,9 +62,16 @@ class Skein::Handler
40
62
  when Hash
41
63
  # Acceptable
42
64
  else
43
- return yield(JSON.dump(
44
- result: nil,
45
- error: 'Request does not conform to the JSON-RPC format.',
65
+ metrics[:failed] += 1
66
+ metrics[:errors][:non_jsonrpc] += 1
67
+ metrics[:time] += state[:finished] - state[:started]
68
+ state[:method] = nil
69
+
70
+ return yield(json_rpc(
71
+ error: {
72
+ code: -32600,
73
+ message: 'Request does not conform to the JSON-RPC format.'
74
+ },
46
75
  id: nil
47
76
  ))
48
77
  end
@@ -58,27 +87,109 @@ class Skein::Handler
58
87
  end
59
88
 
60
89
  unless (request['method'] and request['method'].is_a?(String) and request['method'].match(/\S/))
61
- return yield(JSON.dump(
62
- result: nil,
63
- error: 'Request does not conform to the JSON-RPC format, missing valid method.',
90
+ metrics[:failed] += 1
91
+ metrics[:errors][:no_method_property] += 1
92
+ metrics[:time] += state[:finished] - state[:started]
93
+ state[:method] = nil
94
+
95
+ return yield(json_rpc(
96
+ error: {
97
+ code: -32600,
98
+ message: 'Request does not conform to the JSON-RPC format, missing valid method.'
99
+ },
64
100
  id: request['id']
65
101
  ))
66
102
  end
67
103
 
68
104
  begin
69
- delegate(request['method'], *request['params']) do |result, error = nil|
70
- yield(JSON.dump(
71
- result: result,
72
- error: error,
73
- id: request['id']
74
- ))
105
+ method_name = request['method'].to_sym
106
+ @target.before_execution(method_name)
107
+ state[:method] = method_name
108
+
109
+ delegate(method_name, *request['params']) do |result|
110
+ @target.after_execution(method_name)
111
+ state[:finished] = Time.now
112
+ metrics[:time] += state[:finished] - state[:started]
113
+ state[:method] = nil
114
+
115
+ case (result)
116
+ when Exception
117
+ metrics[:failed] += 1
118
+ metrics[:errors][:exception] += 1
119
+
120
+ yield(json_rpc(
121
+ error: {
122
+ code: -32603,
123
+ message: result.to_s
124
+ },
125
+ id: request['id']
126
+ ))
127
+ else
128
+ metrics[:succeeded] += 1
129
+
130
+ yield(json_rpc(
131
+ result: result,
132
+ id: request['id']
133
+ ))
134
+ end
75
135
  end
136
+
137
+ rescue ArgumentError => e
138
+ # REFACTOR: Make these exception catchers only trap immediate errors,
139
+ # not those that occur within the delegated code.
140
+ @context and @context.exception!(e, message_json)
141
+
142
+ metrics[:failed] += 1
143
+ metrics[:errors][:invalid_arguments] += 1
144
+ state[:finished] = Time.now
145
+ metrics[:time] += state[:finished] - state[:started]
146
+ state[:method] = nil
147
+
148
+ yield(json_rpc(
149
+ error: {
150
+ code: -32602,
151
+ message: 'Invalid params',
152
+ data: {
153
+ method: request['method'],
154
+ params: request['params'],
155
+ message: e.to_s
156
+ }
157
+ },
158
+ id: request['id']
159
+ ))
160
+ rescue NoMethodError => e
161
+ @context and @context.exception!(e, message_json)
162
+
163
+ metrics[:failed] += 1
164
+ metrics[:errors][:invalid_arguments] += 1
165
+ state[:finished] = Time.now
166
+ metrics[:time] += state[:finished] - state[:started]
167
+ state[:method] = nil
168
+
169
+ yield(json_rpc(
170
+ error: {
171
+ code: -32601,
172
+ message: e.to_s,
173
+ data: {
174
+ method: request['method']
175
+ }
176
+ },
177
+ id: request['id']
178
+ ))
76
179
  rescue Object => e
77
180
  @context and @context.exception!(e, message_json)
78
181
 
79
- yield(JSON.dump(
80
- result: nil,
81
- error: '[%s] %s' % [ e.class, e ],
182
+ metrics[:failed] += 1
183
+ metrics[:errors][:exception] += 1
184
+ state[:finished] = Time.now
185
+ metrics[:time] += state[:finished] - state[:started]
186
+ state[:method] = nil
187
+
188
+ yield(json_rpc(
189
+ error: {
190
+ code: -32063,
191
+ message: '[%s] %s' % [ e.class, e ]
192
+ },
82
193
  id: request['id']
83
194
  ))
84
195
  end
@@ -23,6 +23,7 @@ module Skein::RabbitMQ
23
23
  end
24
24
  end
25
25
 
26
+ # REFACTOR: These should be moved to an abstract adapter
26
27
  def self.connect(config = nil)
27
28
  config ||= Skein.config
28
29
 
@@ -0,0 +1,43 @@
1
+ class Skein::TimeoutQueue
2
+ # == Instance Methods =====================================================
3
+
4
+ def initialize(blocking: true, timeout: nil)
5
+ @response = [ ]
6
+ @blocking = blocking
7
+ @timeout = timeout&.to_f
8
+ @mutex = Mutex.new
9
+ @cond_var = ConditionVariable.new
10
+ end
11
+
12
+ def <<(result)
13
+ @mutex.synchronize do
14
+ @response << result
15
+
16
+ @cond_var.signal
17
+ end
18
+ end
19
+
20
+ def pop
21
+ @mutex.synchronize do
22
+ if (@blocking)
23
+ if (@timeout)
24
+ timeout_time = Time.now.to_f + @timeout
25
+
26
+ while (@response.empty? and (remaining_time = timeout_time - Time.now.to_f) > 0)
27
+ @cond_var.wait(@mutex, remaining_time)
28
+ end
29
+ else
30
+ while (@response.empty?)
31
+ @cond_var.wait(@mutex)
32
+ end
33
+ end
34
+ end
35
+
36
+ if (@response.empty?)
37
+ raise Skein::TimeoutException, 'Queue Empty: Time Out'
38
+ end
39
+
40
+ @response.shift
41
+ end
42
+ end
43
+ end
data/lib/skein.rb CHANGED
@@ -1,12 +1,28 @@
1
1
  require 'json'
2
2
 
3
3
  module Skein
4
- VERSION = File.read(File.expand_path('../VERSION', File.dirname(__FILE__))).chomp.freeze
4
+ # == Constants ============================================================
5
+
6
+ VERSION = File.read(File.expand_path('../VERSION', __dir__)).chomp.freeze
7
+
8
+ # == Exceptions ===========================================================
9
+
10
+ class Exception < ::StandardError
11
+ end
12
+
13
+ class TimeoutException < Exception
14
+ end
15
+
16
+ # == Module Methods =======================================================
5
17
 
6
18
  def self.version
7
19
  VERSION
8
20
  end
9
21
 
22
+ def self.config=(config)
23
+ @config = config
24
+ end
25
+
10
26
  def self.config
11
27
  @config ||= Skein::Config.new
12
28
  end
@@ -21,5 +37,5 @@ require_relative './skein/context'
21
37
  require_relative './skein/handler'
22
38
  require_relative './skein/rabbitmq'
23
39
  require_relative './skein/reporter'
24
- require_relative './skein/rpc'
25
40
  require_relative './skein/support'
41
+ require_relative './skein/timeout_queue'
data/skein.gemspec CHANGED
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: skein 0.3.7 ruby lib
5
+ # stub: skein 0.8.1 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "skein".freeze
9
- s.version = "0.3.7"
9
+ s.version = "0.8.1"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Scott Tadman".freeze]
14
- s.date = "2017-03-20"
14
+ s.date = "2020-03-04"
15
15
  s.description = "Wrapper for RabbitMQ that makes blocking RPC calls and handles pub-sub broadcasts.".freeze
16
16
  s.email = "tadman@postageapp.com".freeze
17
17
  s.executables = ["skein".freeze]
@@ -30,6 +30,8 @@ Gem::Specification.new do |s|
30
30
  "bin/skein",
31
31
  "config/.gitignore",
32
32
  "config/skein.yml.example",
33
+ "examples/echo",
34
+ "examples/echo-server",
33
35
  "lib/skein.rb",
34
36
  "lib/skein/adapter.rb",
35
37
  "lib/skein/client.rb",
@@ -45,52 +47,47 @@ Gem::Specification.new do |s|
45
47
  "lib/skein/handler/threaded.rb",
46
48
  "lib/skein/rabbitmq.rb",
47
49
  "lib/skein/reporter.rb",
48
- "lib/skein/rpc.rb",
49
- "lib/skein/rpc/base.rb",
50
- "lib/skein/rpc/error.rb",
51
- "lib/skein/rpc/notification.rb",
52
- "lib/skein/rpc/request.rb",
53
- "lib/skein/rpc/response.rb",
54
50
  "lib/skein/support.rb",
51
+ "lib/skein/timeout_queue.rb",
55
52
  "skein.gemspec",
56
53
  "test/data/sample_config.yml",
57
54
  "test/helper.rb",
58
55
  "test/script/em_example",
59
56
  "test/unit/test_skein_client.rb",
60
57
  "test/unit/test_skein_client_publisher.rb",
58
+ "test/unit/test_skein_client_rpc.rb",
61
59
  "test/unit/test_skein_client_subscriber.rb",
62
60
  "test/unit/test_skein_client_worker.rb",
63
61
  "test/unit/test_skein_config.rb",
62
+ "test/unit/test_skein_connected.rb",
64
63
  "test/unit/test_skein_context.rb",
65
64
  "test/unit/test_skein_rabbitmq.rb",
66
65
  "test/unit/test_skein_reporter.rb",
67
- "test/unit/test_skein_rpc_error.rb",
68
- "test/unit/test_skein_rpc_request.rb",
66
+ "test/unit/test_skein_rpc_timeout.rb",
69
67
  "test/unit/test_skein_support.rb",
70
68
  "test/unit/test_skein_worker.rb",
71
69
  "tmp/.gitignore"
72
70
  ]
73
71
  s.homepage = "http://github.com/postageapp/skein".freeze
74
72
  s.licenses = ["MIT".freeze]
75
- s.rubygems_version = "2.5.2".freeze
73
+ s.rubygems_version = "3.1.2".freeze
76
74
  s.summary = "RabbitMQ RPC/PubSub Library".freeze
77
75
 
78
76
  if s.respond_to? :specification_version then
79
77
  s.specification_version = 4
78
+ end
80
79
 
81
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
82
- s.add_runtime_dependency(%q<birling>.freeze, [">= 0"])
83
- s.add_development_dependency(%q<rake>.freeze, [">= 0"])
84
- s.add_development_dependency(%q<jeweler>.freeze, [">= 0"])
85
- s.add_development_dependency(%q<test-unit>.freeze, [">= 0"])
86
- else
87
- s.add_dependency(%q<birling>.freeze, [">= 0"])
88
- s.add_dependency(%q<rake>.freeze, [">= 0"])
89
- s.add_dependency(%q<jeweler>.freeze, [">= 0"])
90
- s.add_dependency(%q<test-unit>.freeze, [">= 0"])
91
- end
80
+ if s.respond_to? :add_runtime_dependency then
81
+ s.add_runtime_dependency(%q<birling>.freeze, [">= 0.2.0"])
82
+ s.add_development_dependency(%q<bunny>.freeze, [">= 0"])
83
+ s.add_development_dependency(%q<march_hare>.freeze, [">= 0"])
84
+ s.add_development_dependency(%q<rake>.freeze, [">= 0"])
85
+ s.add_development_dependency(%q<jeweler>.freeze, [">= 0"])
86
+ s.add_development_dependency(%q<test-unit>.freeze, [">= 0"])
92
87
  else
93
- s.add_dependency(%q<birling>.freeze, [">= 0"])
88
+ s.add_dependency(%q<birling>.freeze, [">= 0.2.0"])
89
+ s.add_dependency(%q<bunny>.freeze, [">= 0"])
90
+ s.add_dependency(%q<march_hare>.freeze, [">= 0"])
94
91
  s.add_dependency(%q<rake>.freeze, [">= 0"])
95
92
  s.add_dependency(%q<jeweler>.freeze, [">= 0"])
96
93
  s.add_dependency(%q<test-unit>.freeze, [">= 0"])
data/test/helper.rb CHANGED
@@ -39,4 +39,33 @@ class Test::Unit::TestCase
39
39
  def data_path(name)
40
40
  File.expand_path(File.join('data', name), File.dirname(__FILE__))
41
41
  end
42
+
43
+ def wait_for(timeout = 1.0, message = nil)
44
+ sleep_interval = 0.001
45
+ start_time = Time.now
46
+
47
+ while ((Time.now - start_time) < timeout)
48
+ return if (yield)
49
+
50
+ sleep(sleep_interval)
51
+ end
52
+
53
+ fail(message || 'Timed out waiting for condition (%.1fms elapsed)' % [ (Time.now - start_time) * 1000 ])
54
+ end
55
+
56
+ def in_thread
57
+ Thread.new do
58
+ Thread.abort_on_exception = true
59
+
60
+ begin
61
+ yield
62
+
63
+ rescue => e
64
+ $stderr.puts("[%s] %s", [ e.class, e ])
65
+ $stderr.puts(e.backtrace.join("\n"))
66
+
67
+ raise e
68
+ end
69
+ end
70
+ end
42
71
  end