zyre 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'loggability'
5
+
6
+ require 'zyre' unless defined?( Zyre )
7
+
8
+
9
+ #--
10
+ # See also: ext/zyre_ext/poller.c
11
+ class Zyre::Poller
12
+ extend Loggability
13
+
14
+ log_to :zyre
15
+
16
+ end # class Zyre::Poller
@@ -0,0 +1,296 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ require 'securerandom'
5
+ require 'loggability'
6
+
7
+ require 'zyre' unless defined?( Zyre )
8
+
9
+
10
+ # A collection of testing facilities, mostly for RSpec.
11
+ module Zyre::Testing
12
+
13
+ # The minimum number of file descriptors required for testing
14
+ TESTING_FILE_DESCRIPTORS = 4096
15
+
16
+
17
+ # A Factory for generating synthesized ZRE events for testing
18
+ class EventFactory
19
+ extend Loggability
20
+
21
+
22
+ # Default config values
23
+ DEFAULT_HEADERS = {}
24
+ DEFAULT_GROUP = 'default'
25
+ DEFAULT_MSG = 'A message.'
26
+
27
+ # Default config values to use to construct events
28
+ DEFAULT_CONFIG = {
29
+ headers: DEFAULT_HEADERS,
30
+ group: DEFAULT_GROUP,
31
+ msg: DEFAULT_MSG,
32
+ }
33
+
34
+ # The network to pretend events are sent over
35
+ DEFAULT_NETWORK = '10.18.4.0'
36
+
37
+
38
+ # Use the Zyre logger
39
+ log_to :zyre
40
+
41
+
42
+ ### Return the Hash of default config options.
43
+ def self::default_config
44
+ return const_get( :DEFAULT_CONFIG )
45
+ end
46
+
47
+
48
+ ### Return the Hash of default headers to use to construct events which have
49
+ ### them.
50
+ def self::default_headers
51
+ return const_get( :DEFAULT_HEADERS ).dup
52
+ end
53
+
54
+
55
+ ### Return a random network address that's in the DEFAULT_NETWORK
56
+ def self::random_network_address
57
+ last_quad = rand( 1..254 )
58
+ return DEFAULT_NETWORK.sub( /(?<=\.)0$/, last_quad.to_s )
59
+ end
60
+
61
+
62
+ ### Return a port number that's in the ephemeral range.
63
+ def self::random_ephemeral_port
64
+ return rand( 49152 ... 65535)
65
+ end
66
+
67
+
68
+ ### Return a random ZeroMQ-style address that points to an ephemeral port on the
69
+ ### DEFAULT_NETWORK.
70
+ def self::random_addr
71
+ return "tcp://%s:%d" % [ self.random_network_address, self.random_ephemeral_port ]
72
+ end
73
+
74
+
75
+ ### Create a new factory that will use the values from the specified +config+
76
+ ### as defaults when constructing events.
77
+ def initialize( **config )
78
+ config = self.class.default_config.merge( config )
79
+ self.log.debug "Setting up a factory with the config: %p" % [ config ]
80
+
81
+ @peer_uuid = config[:peer_uuid] || SecureRandom.uuid
82
+ @peer_name = config[:peer_name] || "S-%s" % [ @peer_uuid[0, 6] ]
83
+ @peer_addr = config[:peer_addr] || self.class.random_addr
84
+
85
+ @headers = config[:headers]
86
+ @group = config[:group]
87
+ @msg = config[:msg]
88
+ self.log.debug( self )
89
+ end
90
+
91
+
92
+ ######
93
+ public
94
+ ######
95
+
96
+ ##
97
+ # The peer_uuid that's assigned to any events created by this factory
98
+ attr_accessor :peer_uuid
99
+
100
+ ##
101
+ # The peer_name that's assigned to any events created by this factory
102
+ attr_accessor :peer_name
103
+
104
+ ##
105
+ # The peer_addr that's assigned to any events created by this factory
106
+ attr_accessor :peer_addr
107
+
108
+ ##
109
+ # The Hash of headers to set on any events created by this factory which have
110
+ # headers
111
+ attr_accessor :headers
112
+
113
+ ##
114
+ # The name of the group set on any events created by this factory which have a
115
+ # group
116
+ attr_accessor :group
117
+
118
+ ##
119
+ # The message data to set on any events create by this factory that have a
120
+ # `msg`.
121
+ attr_accessor :msg
122
+
123
+
124
+ ### Returns a Hash of the configured #headers with stringified keys and values.
125
+ def normalized_headers
126
+ return Zyre.normalize_headers( self.headers )
127
+ end
128
+
129
+
130
+ ### Generate an ENTER event.
131
+ def enter( **overrides )
132
+ headers = overrides.delete( :headers ) || self.headers
133
+ uuid = overrides.delete( :peer_uuid ) || self.peer_uuid
134
+ config = {
135
+ peer_name: self.peer_name,
136
+ peer_addr: self.peer_addr,
137
+ headers: Zyre.normalize_headers( headers )
138
+ }.merge( overrides )
139
+
140
+ return Zyre::Event.synthesize( :enter, uuid, **config )
141
+ end
142
+
143
+
144
+ ### Generate a JOIN event.
145
+ def join( **overrides )
146
+ uuid = overrides.delete( :peer_uuid ) || self.peer_uuid
147
+ config = {
148
+ peer_name: self.peer_name,
149
+ group: self.group
150
+ }.merge( overrides )
151
+
152
+ return Zyre::Event.synthesize( :join, uuid, **config )
153
+ end
154
+
155
+
156
+ ### Generate a SHOUT event.
157
+ def shout( **overrides )
158
+ uuid = overrides.delete( :peer_uuid ) || self.peer_uuid
159
+ config = {
160
+ peer_name: self.peer_name,
161
+ group: self.group,
162
+ msg: self.msg
163
+ }.merge( overrides )
164
+
165
+ return Zyre::Event.synthesize( :shout, uuid, **config )
166
+ end
167
+
168
+
169
+ ### Generate a WHISPER event.
170
+ def whisper( **overrides )
171
+ uuid = overrides.delete( :peer_uuid ) || self.peer_uuid
172
+ config = {
173
+ peer_name: self.peer_name,
174
+ msg: self.msg
175
+ }.merge( overrides )
176
+
177
+ return Zyre::Event.synthesize( :whisper, uuid, **config )
178
+ end
179
+
180
+
181
+ ### Generate an EVASIVE event.
182
+ def evasive( **overrides )
183
+ uuid = overrides.delete( :peer_uuid ) || self.peer_uuid
184
+ config = {
185
+ peer_name: self.peer_name
186
+ }.merge( overrides )
187
+
188
+ return Zyre::Event.synthesize( :evasive, uuid, **config )
189
+ end
190
+
191
+
192
+ ### Generate a SILENT event.
193
+ def silent( **overrides )
194
+ uuid = overrides.delete( :peer_uuid ) || self.peer_uuid
195
+ config = {
196
+ peer_name: self.peer_name
197
+ }.merge( overrides )
198
+
199
+ return Zyre::Event.synthesize( :silent, uuid, **config )
200
+ end
201
+
202
+
203
+ ### Generate a LEAVE event.
204
+ def leave( **overrides )
205
+ uuid = overrides.delete( :peer_uuid ) || self.peer_uuid
206
+ config = {
207
+ peer_name: self.peer_name,
208
+ group: self.group
209
+ }.merge( overrides )
210
+
211
+ return Zyre::Event.synthesize( :leave, uuid, **config )
212
+ end
213
+
214
+
215
+ ### Generate an EXIT event.
216
+ def exit( **overrides )
217
+ uuid = overrides.delete( :peer_uuid ) || self.peer_uuid
218
+ config = {
219
+ peer_name: self.peer_name
220
+ }.merge( overrides )
221
+
222
+ return Zyre::Event.synthesize( :exit, uuid, **config )
223
+ end
224
+
225
+ end # class EventFactory
226
+
227
+
228
+ ### Add hooks when the given +context+ has helpers added to it.
229
+ def self::included( context )
230
+
231
+ context.let( :gossip_hub ) { "inproc://gossip-hub-%s" % [ SecureRandom.hex(16) ] }
232
+
233
+ context.before( :each ) do
234
+ @gossip_endpoint = nil
235
+ @started_zyre_nodes = []
236
+ end
237
+
238
+ context.after( :each ) do
239
+ @started_zyre_nodes.each( &:stop )
240
+ end
241
+
242
+ super
243
+ end
244
+
245
+
246
+ ###############
247
+ module_function
248
+ ###############
249
+
250
+ ### Return a node that's been configured and started.
251
+ def started_node( name=nil )
252
+ node = Zyre::Node.new( name )
253
+ node.endpoint = 'inproc://node-test-%s' % [ SecureRandom.hex(16) ]
254
+ yield( node ) if block_given?
255
+
256
+ if @gossip_endpoint
257
+ # $stderr.puts "Connecting to %p" % [ @gossip_endpoint ]
258
+ node.gossip_connect( @gossip_endpoint )
259
+ else
260
+ @gossip_endpoint = gossip_hub()
261
+ # $stderr.puts "Binding to %p" % [ @gossip_endpoint ]
262
+ node.gossip_bind( @gossip_endpoint )
263
+ sleep 0.25
264
+ end
265
+
266
+ # $stderr.puts "Starting %p" % [ node ]
267
+ node.start
268
+ @started_zyre_nodes << node
269
+
270
+ return node
271
+ end
272
+
273
+
274
+ ### Reset file descriptor limit higher for OSes that have low limits, e.g., OSX.
275
+ ### Refs:
276
+ ### - http://wiki.zeromq.org/docs:tuning-zeromq#toc1
277
+ def check_fdmax
278
+ current_fdmax, max_fdmax = Process.getrlimit( :NOFILE )
279
+
280
+ if max_fdmax < TESTING_FILE_DESCRIPTORS
281
+ warn <<~END_WARNING
282
+ >>>
283
+ >>> Can't set file-descriptor ulimit to #{TESTING_FILE_DESCRIPTORS}. Later specs
284
+ >>> might fail due to lack of file descriptors.
285
+ >>>
286
+ END_WARNING
287
+ else
288
+ Process.setrlimit( :NOFILE, TESTING_FILE_DESCRIPTORS ) if
289
+ current_fdmax < TESTING_FILE_DESCRIPTORS
290
+ end
291
+ end
292
+
293
+
294
+
295
+ end # module Zyre::Testing
296
+
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../../spec_helper'
4
+
5
+
6
+ RSpec.describe 'Observability::Instrumentation::Zyre', :observability do
7
+
8
+ before( :all ) do
9
+ Observability.install_instrumentation( :zyre )
10
+ end
11
+
12
+ before( :each ) do
13
+ Observability.observer.sender.enqueued_events.clear
14
+ end
15
+
16
+
17
+ let( :described_class ) { Observability::Instrumentation::Zyre }
18
+
19
+
20
+ it "is available" do
21
+ expect( described_class ).to be_available
22
+ end
23
+
24
+
25
+ it "records the peer ID and message when a whisper is sent" do
26
+ node1 = started_node()
27
+ node2 = started_node()
28
+
29
+ node1.whisper( node2.uuid, "a peer-to-peer message" )
30
+
31
+ events = Observability.observer.sender.find_events( 'zyre.node.whisper' )
32
+ expect( events.length ).to eq( 1 )
33
+ expect( events.first[:peer_uuid] ).to eq( node2.uuid )
34
+ expect( events.first[:message] ).to eq( 'a peer-to-peer message' )
35
+
36
+ end
37
+
38
+
39
+ it "records the group and message when a shout is sent" do
40
+ node1 = started_node()
41
+ node1.join( 'observer-testing' )
42
+ node2 = started_node()
43
+ node2.join( 'observer-testing' )
44
+
45
+ node1.wait_for( :JOIN, timeout: 2.0, peer_uuid: node2.uuid )
46
+
47
+ node1.shout( 'observer-testing', "a peer-to-peer message" )
48
+
49
+ events = Observability.observer.sender.find_events( 'zyre.node.shout' )
50
+ expect( events.length ).to eq( 1 )
51
+ expect( events.first[:group] ).to eq( 'observer-testing' )
52
+ expect( events.first[:message] ).to eq( 'a peer-to-peer message' )
53
+ end
54
+
55
+ end
56
+
@@ -0,0 +1,62 @@
1
+ # -*- ruby -*-
2
+ # frozen_string_literal: true
3
+
4
+ if ENV['COVERAGE'] || ENV['CI']
5
+ require 'simplecov'
6
+ if ENV['CI']
7
+ require 'simplecov-cobertura'
8
+ SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter
9
+ end
10
+ end
11
+
12
+ require 'rspec'
13
+ require 'rspec/wait'
14
+ require 'zyre'
15
+ require 'zyre/testing'
16
+
17
+ require 'securerandom'
18
+ require 'loggability/spechelpers'
19
+
20
+
21
+ begin
22
+ require 'observability'
23
+ $have_observability = true
24
+
25
+ Observability::Sender.configure( type: :testing )
26
+ rescue LoadError => err
27
+ $have_observability = false
28
+ end
29
+
30
+
31
+ ### Mock with RSpec
32
+ RSpec.configure do |config|
33
+ config.expect_with :rspec do |expectations|
34
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
35
+ expectations.syntax = :expect
36
+ end
37
+
38
+ config.mock_with( :rspec ) do |mock|
39
+ mock.syntax = :expect
40
+ mock.verify_partial_doubles = true
41
+ end
42
+
43
+ config.disable_monkey_patching!
44
+ config.example_status_persistence_file_path = "spec/.status"
45
+ config.filter_run :focus
46
+ config.filter_run_excluding :observability unless $have_observability
47
+ config.filter_run_when_matching :focus
48
+ config.order = :random
49
+ config.profile_examples = 5
50
+ config.run_all_when_everything_filtered = true
51
+ config.shared_context_metadata_behavior = :apply_to_host_groups
52
+ # config.warnings = true
53
+
54
+ config.before( :suite ) do
55
+ Zyre::Testing.check_fdmax
56
+ end
57
+
58
+ config.include( Zyre::Testing )
59
+ config.include( Loggability::SpecHelpers )
60
+ end
61
+
62
+
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../spec_helper'
4
+
5
+ require 'securerandom'
6
+ require 'zyre'
7
+
8
+
9
+ RSpec.describe( Zyre::Event ) do
10
+
11
+ describe "concrete event classes" do
12
+
13
+ it "can look up subtypes by upcased String name" do
14
+ expect( described_class.type_by_name('ENTER') ).to eq( described_class::Enter )
15
+ expect( described_class.type_by_name('EVASIVE') ).to eq( described_class::Evasive )
16
+ expect( described_class.type_by_name('SILENT') ).to eq( described_class::Silent )
17
+ expect( described_class.type_by_name('EXIT') ).to eq( described_class::Exit )
18
+ expect( described_class.type_by_name('JOIN') ).to eq( described_class::Join )
19
+ expect( described_class.type_by_name('LEAVE') ).to eq( described_class::Leave )
20
+ expect( described_class.type_by_name('WHISPER') ).to eq( described_class::Whisper )
21
+ expect( described_class.type_by_name('SHOUT') ).to eq( described_class::Shout )
22
+ end
23
+
24
+
25
+ it "can look up subtypes by downcased String name" do
26
+ expect( described_class.type_by_name('enter') ).to eq( described_class::Enter )
27
+ expect( described_class.type_by_name('evasive') ).to eq( described_class::Evasive )
28
+ expect( described_class.type_by_name('silent') ).to eq( described_class::Silent )
29
+ expect( described_class.type_by_name('exit') ).to eq( described_class::Exit )
30
+ expect( described_class.type_by_name('join') ).to eq( described_class::Join )
31
+ expect( described_class.type_by_name('leave') ).to eq( described_class::Leave )
32
+ expect( described_class.type_by_name('whisper') ).to eq( described_class::Whisper )
33
+ expect( described_class.type_by_name('shout') ).to eq( described_class::Shout )
34
+ end
35
+
36
+
37
+ it "can look up subtypes by upcased Symbol name" do
38
+ expect( described_class.type_by_name(:Enter) ).to eq( described_class::Enter )
39
+ expect( described_class.type_by_name(:Evasive) ).to eq( described_class::Evasive )
40
+ expect( described_class.type_by_name(:Silent) ).to eq( described_class::Silent )
41
+ expect( described_class.type_by_name(:Exit) ).to eq( described_class::Exit )
42
+ expect( described_class.type_by_name(:Join) ).to eq( described_class::Join )
43
+ expect( described_class.type_by_name(:Leave) ).to eq( described_class::Leave )
44
+ expect( described_class.type_by_name(:Whisper) ).to eq( described_class::Whisper )
45
+ expect( described_class.type_by_name(:Shout) ).to eq( described_class::Shout )
46
+ end
47
+
48
+
49
+ it "can look up subtypes by downcased Symbol name" do
50
+ expect( described_class.type_by_name(:enter) ).to eq( described_class::Enter )
51
+ expect( described_class.type_by_name(:evasive) ).to eq( described_class::Evasive )
52
+ expect( described_class.type_by_name(:silent) ).to eq( described_class::Silent )
53
+ expect( described_class.type_by_name(:exit) ).to eq( described_class::Exit )
54
+ expect( described_class.type_by_name(:join) ).to eq( described_class::Join )
55
+ expect( described_class.type_by_name(:leave) ).to eq( described_class::Leave )
56
+ expect( described_class.type_by_name(:whisper) ).to eq( described_class::Whisper )
57
+ expect( described_class.type_by_name(:shout) ).to eq( described_class::Shout )
58
+ end
59
+
60
+
61
+ it "can return its type as a String" do
62
+ expect( described_class.type_by_name(:enter).type_name ).to eq( 'ENTER' )
63
+ expect( described_class.type_by_name(:evasive).type_name ).to eq( 'EVASIVE' )
64
+ expect( described_class.type_by_name(:silent).type_name ).to eq( 'SILENT' )
65
+ expect( described_class.type_by_name(:exit).type_name ).to eq( 'EXIT' )
66
+ expect( described_class.type_by_name(:join).type_name ).to eq( 'JOIN' )
67
+ expect( described_class.type_by_name(:leave).type_name ).to eq( 'LEAVE' )
68
+ expect( described_class.type_by_name(:whisper).type_name ).to eq( 'WHISPER' )
69
+ expect( described_class.type_by_name(:shout).type_name ).to eq( 'SHOUT' )
70
+ end
71
+
72
+
73
+ it "returns nil for non-existent subtypes" do
74
+ expect( described_class.type_by_name(:boom) ).to be_nil
75
+ end
76
+
77
+ end
78
+
79
+
80
+ describe "matching API" do
81
+
82
+ it "matches on a single criterion" do
83
+ node1 = started_node()
84
+ node1.join( 'matching-test' )
85
+
86
+ node2 = started_node()
87
+ node2.join( 'matching-test' )
88
+
89
+ event = node1.wait_for( :JOIN )
90
+
91
+ expect( event ).to match( peer_uuid: node2.uuid )
92
+ expect( event ).not_to match( peer_uuid: node1.uuid )
93
+ end
94
+
95
+
96
+ it "matches on multiple criterion" do
97
+ node1 = started_node( 'badger-2' )
98
+ node1.join( 'matching-test2' )
99
+
100
+ node2 = started_node( 'badger-6' )
101
+ node2.join( 'matching-test2' )
102
+
103
+ event = node1.wait_for( :JOIN )
104
+
105
+ expect( event ).to match( peer_uuid: node2.uuid, peer_name: 'badger-6' )
106
+ expect( event ).not_to match( peer_uuid: node2.uuid, peer_name: 'badger-2' )
107
+ end
108
+
109
+
110
+ it "matches Regexp patterns as values"
111
+
112
+ end
113
+
114
+
115
+ describe "synthesis API" do
116
+
117
+ let( :peer_uuid ) { '8D9B6F67-2B40-4E56-B352-39029045B568' }
118
+
119
+
120
+ it "can generate events without a node" do
121
+ result = described_class.synthesize( :ENTER,
122
+ peer_uuid, peer_name: 'node1', peer_addr: 'in-proc:/synthesized' )
123
+
124
+ expect( result ).to be_a( described_class::Enter )
125
+ expect( result.peer_uuid ).to eq( peer_uuid )
126
+ expect( result.peer_name ).to eq( 'node1' )
127
+ expect( result.peer_addr ).to eq( 'in-proc:/synthesized' )
128
+ end
129
+
130
+
131
+ it "defaults its peer_name to S- + six characters of the peer_uuid" do
132
+ result = described_class.synthesize( :ENTER,
133
+ peer_uuid, peer_addr: 'in-proc:/synthesized' )
134
+
135
+ expect( result.peer_name ).to eq( 'S-' + peer_uuid[0, 6] )
136
+ end
137
+
138
+ end
139
+
140
+ end
141
+