zyre 0.1.0

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.
@@ -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
+