haredo 0.0.5 → 2.0.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.
- checksums.yaml +4 -4
- data/README.md +355 -16
- data/src/bin/haredo +115 -0
- data/src/lib/haredo/admin/config.rb +178 -0
- data/src/lib/haredo/admin/interface.rb +25 -0
- data/src/lib/haredo/config.rb +54 -0
- data/src/lib/haredo/peer.rb +210 -110
- data/src/lib/haredo/plugin.rb +19 -0
- data/src/lib/haredo/plugins/echo.rb +15 -0
- data/src/lib/haredo/plugins/example.rb +17 -0
- data/src/lib/haredo/plugins/status.rb +31 -0
- data/src/lib/haredo/service/admin/daemon.rb +128 -0
- data/src/lib/haredo/service/config.rb +29 -0
- data/src/lib/haredo/service/daemon.rb +141 -0
- data/src/lib/haredo/version.rb +7 -1
- metadata +19 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ff09def9c61a951c40c71dd2ce8873f6883e84e4
|
4
|
+
data.tar.gz: 9e2a10e846685afb1b5b238117aaecf15f9b4116
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4bea5b65ac4a075d7a317186163e222b7eeab387f163d6c56c3b363d90f29047fdf46c87a11f10758b2ed766c4b3425c6998c16bf538ba355d890d98fab04107
|
7
|
+
data.tar.gz: 13fa22657bfe7fa540c30a5570a90cfe185f17fbf6580ab94532d85b1d873853d3bdc94f64682302f5cd025e5fbf1a65bc3ca55fe45d28be4d1d075f6ef94c8a
|
data/README.md
CHANGED
@@ -45,7 +45,7 @@ $mq_username = 'guest'
|
|
45
45
|
$mq_password = 'guest'
|
46
46
|
$queue = 'HareDo'
|
47
47
|
|
48
|
-
class Service < HareDo::
|
48
|
+
class Service < HareDo::Peer
|
49
49
|
|
50
50
|
def initialize(name)
|
51
51
|
super
|
@@ -60,9 +60,9 @@ class Service < HareDo::Service
|
|
60
60
|
end
|
61
61
|
|
62
62
|
service = Service.new($queue)
|
63
|
-
service.
|
63
|
+
service.listen(:blocking => false)
|
64
64
|
|
65
|
-
client = HareDo::
|
65
|
+
client = HareDo::Peer.new()
|
66
66
|
client.connect(:user=>$mq_username, :password=>$mq_password, :host=>$mq_host)
|
67
67
|
|
68
68
|
1.upto(10) do |i|
|
@@ -71,6 +71,10 @@ client.connect(:user=>$mq_username, :password=>$mq_password, :host=>$mq_host)
|
|
71
71
|
|
72
72
|
puts msg.data.to_i == i + 1
|
73
73
|
end
|
74
|
+
|
75
|
+
# Very important -- you will leave queues open on RabbitMQ if you don't do this
|
76
|
+
service.disconnect()
|
77
|
+
client.disconnect()
|
74
78
|
```
|
75
79
|
|
76
80
|
The service takes a single argument -- the a name. This name is the name of the
|
@@ -92,7 +96,7 @@ quick and easy network protocols with minimal code (thanks to Rabbit and Bunny).
|
|
92
96
|
|
93
97
|
There are three basic use-cases the client addresses:
|
94
98
|
|
95
|
-
* Event Dispatch: Client sends one or messages expecting
|
99
|
+
* Event Dispatch: Client sends one or messages expecting no response. This is
|
96
100
|
the case when you want to log events -- you don't really care about getting a
|
97
101
|
response back. You just want to send out the event and be done with it.
|
98
102
|
* Send/Receive: Client sends one or more messages expecting response and
|
@@ -122,7 +126,7 @@ $mq_host = 'localhost'
|
|
122
126
|
$mq_username = 'guest'
|
123
127
|
$mq_password = 'guest'
|
124
128
|
|
125
|
-
class
|
129
|
+
class Service < HareDo::Peer
|
126
130
|
|
127
131
|
def initialize(name)
|
128
132
|
super
|
@@ -135,7 +139,8 @@ class Logger < HareDo::Service
|
|
135
139
|
|
136
140
|
end
|
137
141
|
|
138
|
-
Service.new('logger').
|
142
|
+
Service.new('logger').listen()
|
143
|
+
service.disconnect()
|
139
144
|
```
|
140
145
|
|
141
146
|
The client code is a follows:
|
@@ -149,10 +154,11 @@ $mq_host = 'localhost'
|
|
149
154
|
$mq_username = 'guest'
|
150
155
|
$mq_password = 'guest'
|
151
156
|
|
152
|
-
client = HareDo::
|
157
|
+
client = HareDo::Peer.new()
|
153
158
|
client.connect(:user=>$mq_username, :password=>$mq_password, :host=>$mq_host)
|
154
159
|
|
155
160
|
client.send('logger', :data => 'Backup script failed')
|
161
|
+
client.disconnect()
|
156
162
|
```
|
157
163
|
|
158
164
|
You can run both in the same script by setting the service code to non-blocking
|
@@ -172,7 +178,7 @@ $mq_host = 'localhost'
|
|
172
178
|
$mq_username = 'guest'
|
173
179
|
$mq_password = 'guest'
|
174
180
|
|
175
|
-
class
|
181
|
+
class Service < HareDo::Peer
|
176
182
|
|
177
183
|
def initialize(name)
|
178
184
|
super
|
@@ -185,11 +191,14 @@ class Logger < HareDo::Service
|
|
185
191
|
|
186
192
|
end
|
187
193
|
|
188
|
-
Service.new('logger').
|
194
|
+
Service.new('logger').listen(:blocking=>false)
|
189
195
|
|
190
|
-
client = HareDo::
|
196
|
+
client = HareDo::Peer.new()
|
191
197
|
client.connect(:user=>$mq_username, :password=>$mq_password, :host=>$mq_host)
|
192
198
|
client.send('logger', :data => 'Backup script failed')
|
199
|
+
|
200
|
+
service.disconnect()
|
201
|
+
client.disconnect()
|
193
202
|
```
|
194
203
|
|
195
204
|
### Send/Receive
|
@@ -216,7 +225,7 @@ $mq_host = 'localhost'
|
|
216
225
|
$mq_username = 'guest'
|
217
226
|
$mq_password = 'guest'
|
218
227
|
|
219
|
-
class Service < HareDo::
|
228
|
+
class Service < HareDo::Peer
|
220
229
|
|
221
230
|
def initialize(name)
|
222
231
|
super
|
@@ -229,11 +238,14 @@ class Service < HareDo::Service
|
|
229
238
|
end
|
230
239
|
|
231
240
|
service = Service.new('rpc')
|
232
|
-
service.
|
241
|
+
service.listen(:blocking => false)
|
233
242
|
|
234
|
-
client = HareDo::
|
243
|
+
client = HareDo::Peer.new()
|
235
244
|
client.connect(:user=>$mq_username, :password=>$mq_password, :host=>$mq_host)
|
236
245
|
response = @client.call('rpc', :data => 'jujifruit')
|
246
|
+
|
247
|
+
client.disconnect()
|
248
|
+
service.disconnect()
|
237
249
|
```
|
238
250
|
|
239
251
|
The <tt>call()</tt> method will block waiting for the reponse up to
|
@@ -271,7 +283,7 @@ addresses a message back to the client. It also adds a header value of
|
|
271
283
|
is specifically a reply. This is needed this because what if another peer sent
|
272
284
|
the client a message which happened to have the same message ID? The client
|
273
285
|
would confuse that message as the reply from the service and that would mess
|
274
|
-
things up fast. Therefore the <tt>reply
|
286
|
+
things up fast. Therefore the <tt>reply</tt> flag lets the client know that the
|
275
287
|
message is a reply to a previous request and can then use the message ID to
|
276
288
|
correlate it with the original request.
|
277
289
|
|
@@ -296,7 +308,7 @@ $mq_host = 'localhost'
|
|
296
308
|
$mq_username = 'guest'
|
297
309
|
$mq_password = 'guest'
|
298
310
|
|
299
|
-
class Service < HareDo::
|
311
|
+
class Service < HareDo::Peer
|
300
312
|
|
301
313
|
def initialize(name)
|
302
314
|
super
|
@@ -312,7 +324,8 @@ class Service < HareDo::Service
|
|
312
324
|
end
|
313
325
|
end
|
314
326
|
|
315
|
-
Service.new('rpc').
|
327
|
+
Service.new('rpc').listen()
|
328
|
+
service.disconnect()
|
316
329
|
```
|
317
330
|
|
318
331
|
The only thing we did is remove the <tt>exclusive</tt> attribute which is by
|
@@ -322,6 +335,332 @@ process requests. RabbitMQ will automatically distribute messages equally across
|
|
322
335
|
all the running services, dividing up the load. You can now scale your service
|
323
336
|
to as many machines and processes as you like.
|
324
337
|
|
338
|
+
## Peers
|
339
|
+
|
340
|
+
In case you haven't noticed by now, there is no distinction in code between a
|
341
|
+
service and client. Everything is implemented in the same class:
|
342
|
+
<tt>HareDo::Peer</tt>. So the only difference between a client and service is
|
343
|
+
how you use the <tt>Peer</tt> class. To create custom services, you just derive
|
344
|
+
from <tt>Peer</tt> and implement a <tt>serve()</tt> method. Or, you can get away
|
345
|
+
with not even doing that -- you could just write a plugin.
|
346
|
+
|
347
|
+
## Plugins
|
348
|
+
|
349
|
+
The library includes a simple plugin architecture which is included in the
|
350
|
+
<tt>Peer</tt> class. This allows message processing to be delegated to external
|
351
|
+
modules written outside the library.
|
352
|
+
|
353
|
+
For example, I could write a simple echo plugin like this:
|
354
|
+
|
355
|
+
```ruby
|
356
|
+
require 'haredo/plugin'
|
357
|
+
|
358
|
+
class Plugin < HareDo::Plugin
|
359
|
+
|
360
|
+
UUID = 'echo'
|
361
|
+
|
362
|
+
def initialize(service, config)
|
363
|
+
super UUID, service, config
|
364
|
+
end
|
365
|
+
|
366
|
+
def process(msg)
|
367
|
+
@peer.reply(msg, :data=>msg.data)
|
368
|
+
end
|
369
|
+
|
370
|
+
end # class Plugin
|
371
|
+
```
|
372
|
+
|
373
|
+
Plugins are loaded in anonymous modules at runtime, each having its own private
|
374
|
+
namespaces. The <tt>Peer</tt> class then looks inside the anonymous module and
|
375
|
+
extracts the <tt>Plugin</tt> class defined within, instantiates it, and adds it
|
376
|
+
to the set of loaded modules.
|
377
|
+
|
378
|
+
Plugins are identified by their UUID. To send a message to a plugin, you include
|
379
|
+
the UUID of the plugin in the message headers.
|
380
|
+
|
381
|
+
Each plugin is passed a reference to the peer that uses them on
|
382
|
+
construction. This is stored in the <tt>@peer</tt> member. This is the message
|
383
|
+
gateway that your plugin uses to send/reply with.
|
384
|
+
|
385
|
+
Now I can implement my service and call it as follows:
|
386
|
+
|
387
|
+
```ruby
|
388
|
+
#!/usr/bin/env ruby
|
389
|
+
|
390
|
+
service = HareDo::Peer.new('myservice')
|
391
|
+
service.plugins.load('echo')
|
392
|
+
service.listen(:blocking => false)
|
393
|
+
|
394
|
+
client = HareDo::Peer.new()
|
395
|
+
client.connect(:user=>$mq_username, :password=>$mq_password, :host=>$mq_host)
|
396
|
+
response = @client.call('myservice', headers=>{:uuid=>'echo'}, :data => 'jujifruit')
|
397
|
+
|
398
|
+
client.disconnect()
|
399
|
+
service.disconnect()
|
400
|
+
```
|
401
|
+
|
402
|
+
In this case, I didn't even need to implement a custom service. I just loaded my
|
403
|
+
plugin. By default the base class implementation of
|
404
|
+
<tt>HareDo::Peer::serve()</tt> passes all messages into the plugin system. If no
|
405
|
+
plugin is found to service the message, it is simply dropped.
|
406
|
+
|
407
|
+
By default the <tt>Peer::plugins::load()</tt> method iterates over each entry in
|
408
|
+
Ruby <tt>PATH</tt> and looks within the <tt>haredo/plugins</tt> directly of each
|
409
|
+
entry. This value is given by the <tt>@module_path_prefix</tt> member of
|
410
|
+
<tt>Peer</tt>, which you can modify to suit your needs. Therefore, if you stored
|
411
|
+
your plugins in <tt>/opt/share/haredo/plugins</tt> and you wanted to load from
|
412
|
+
there, you could do it a couple different ways:
|
413
|
+
|
414
|
+
Add <tt>/opt/share</tt> to your Ruby path:
|
415
|
+
|
416
|
+
```ruby
|
417
|
+
$:.unshift '/opt/share'
|
418
|
+
service.plugins.load('echo')
|
419
|
+
```
|
420
|
+
|
421
|
+
Add <tt>/opt/share</tt> to your Ruby path and modify the service's
|
422
|
+
<tt>@module_path_prefix</tt>:
|
423
|
+
|
424
|
+
```ruby
|
425
|
+
$:.unshift '/opt/share/haredo/plugins'
|
426
|
+
|
427
|
+
# Remove the module prefix.
|
428
|
+
service.module_path_prefix = ''
|
429
|
+
|
430
|
+
service.plugins.load('echo')
|
431
|
+
```
|
432
|
+
|
433
|
+
In this case, every module in the Ruby path that has the name <tt>echo.rb</tt>
|
434
|
+
will be a candidate. The first file to match will be loaded as the module. You
|
435
|
+
can see that in this case, namespace collisions are a very real danger. That's
|
436
|
+
why the <tt>@module_path_prefix</tt> member exists -- there is a very low
|
437
|
+
probability that there will be another module named located in the Ruby path
|
438
|
+
which has a relative directory and name of <tt>haredo/plugins/echo.rb</tt>.
|
439
|
+
|
440
|
+
### Configuration
|
441
|
+
|
442
|
+
You can pass configuration data into a plugin when you load it using the
|
443
|
+
<tt>Peer::plugins.loadConfig()</tt> method. This takes a HASH whose keys are the
|
444
|
+
names of the plugins to load and the values are Hashes passed into each plugin
|
445
|
+
as the configuration. The contents of the configuration are complete up to you
|
446
|
+
as the plugin author.
|
447
|
+
|
448
|
+
For example:
|
449
|
+
|
450
|
+
```ruby
|
451
|
+
#!/usr/bin/env ruby
|
452
|
+
|
453
|
+
service = HareDo::Peer.new('myservice')
|
454
|
+
|
455
|
+
# Load modules from configuration
|
456
|
+
config = {
|
457
|
+
# module name => module configuration
|
458
|
+
'example' => { 'x' => '1' },
|
459
|
+
'echo' => { 'y' => '2' }
|
460
|
+
}
|
461
|
+
|
462
|
+
service.plugins.loadConfig(config)
|
463
|
+
service.listen(:blocking => false)
|
464
|
+
|
465
|
+
client = HareDo::Peer.new()
|
466
|
+
client.connect(:user=>$mq_username, :password=>$mq_password, :host=>$mq_host)
|
467
|
+
response = @client.call('myservice', headers=>{:uuid=>'example'}, :data => 'jujifruit')
|
468
|
+
|
469
|
+
service.disconnect()
|
470
|
+
client.disconnect()
|
471
|
+
```
|
472
|
+
|
473
|
+
By using this method to load plugins, you can store all your plugin
|
474
|
+
configuration in a YAML file and load it at runtime to load/configure all of
|
475
|
+
your plugins for your service(s).
|
476
|
+
|
477
|
+
There is nothing stopping you from using plugins within a client context as
|
478
|
+
well. If you get a message in from a peer, you can simply check for a UUID in
|
479
|
+
the message headers and pass it to <tt>@plugins.process()</tt> if you so choose.
|
480
|
+
|
481
|
+
The plugin architecture is a powerful way to implement and add an endless
|
482
|
+
variety of services with minimal code.
|
483
|
+
|
484
|
+
A complete example of using plugins is illustrated in the <tt>basic.rb</tt> unit
|
485
|
+
test with the source distribution.
|
486
|
+
|
487
|
+
## Daemons
|
488
|
+
|
489
|
+
Say you have a service written and now you want an easy way to start/stop/manage
|
490
|
+
it. The <tt>haredo</tt> command-line utility is included just for that. You can
|
491
|
+
add your service to a simple configuration file, run <tt>haredo start</tt> and
|
492
|
+
your service is online. The default configuration file is in
|
493
|
+
<tt>/etc/haredo/haredo.conf</tt> and looks something like this:
|
494
|
+
|
495
|
+
```yaml
|
496
|
+
---
|
497
|
+
system:
|
498
|
+
broker:
|
499
|
+
host: localhost
|
500
|
+
user: guest
|
501
|
+
password: guest
|
502
|
+
port: '5672'
|
503
|
+
daemon:
|
504
|
+
queue:
|
505
|
+
name: 'jujifruit'
|
506
|
+
properties:
|
507
|
+
exclusive: false
|
508
|
+
auto_delete: true
|
509
|
+
key: 'b8d96b66-8cae-4e16-b5ec-d607483b061e'
|
510
|
+
modules:
|
511
|
+
- sonar/service/config
|
512
|
+
services:
|
513
|
+
haredo:
|
514
|
+
path_prefix: haredo/plugins
|
515
|
+
plugins:
|
516
|
+
echo:
|
517
|
+
juji: fruit
|
518
|
+
myplugins:
|
519
|
+
path_prefix: mylib/plugins
|
520
|
+
plugins:
|
521
|
+
log:
|
522
|
+
db:
|
523
|
+
host: localhost
|
524
|
+
db: sonar
|
525
|
+
user: root
|
526
|
+
```
|
527
|
+
|
528
|
+
### The Listen Queue
|
529
|
+
|
530
|
+
The daemon creates a queue using the <tt>queue</tt> entries. The name is given
|
531
|
+
by the <tt>name</tt> attribute and the other optional queue properties are given
|
532
|
+
by the <tt>properties</tt> attributes. The will be applied to the daemon's
|
533
|
+
listen queue.
|
534
|
+
|
535
|
+
### Services
|
536
|
+
|
537
|
+
Servces are sets of plugins that are loaded from different places. Usually these
|
538
|
+
are delineated by the need to change the <tt>path_prefix</tt> value to load a
|
539
|
+
given set of modules. At load time, the daemon iterates over each entry in
|
540
|
+
services, modified the <tt>path_prefix</tt> according to the given value and
|
541
|
+
then attempts to load all the plugins listed. It prints the results for each
|
542
|
+
plugin to standard error. If a plugin fails to load, it will also log this in
|
543
|
+
syslog.
|
544
|
+
|
545
|
+
### Modules
|
546
|
+
|
547
|
+
Modules are additional modules you want to daemon to load. This is done before
|
548
|
+
services are processed. This is for when you want to modify the daemon's code in
|
549
|
+
some way (adding or overriding methods), or also including some custom
|
550
|
+
initialization. For example, say I have a module <tt>sonar/service/config</tt>
|
551
|
+
that adds database connection functionality to <tt>HareDo::Service::Config</tt>
|
552
|
+
mixin as follows:
|
553
|
+
|
554
|
+
```ruby
|
555
|
+
require 'jw/dbi'
|
556
|
+
|
557
|
+
module HareDo
|
558
|
+
module Service
|
559
|
+
|
560
|
+
module Config
|
561
|
+
|
562
|
+
# Connect to the Database
|
563
|
+
#
|
564
|
+
# @return Return a client instance if connection was successful, nil otherwise
|
565
|
+
def connectDb(params)
|
566
|
+
|
567
|
+
vars = {
|
568
|
+
host: params['host'],
|
569
|
+
db: params['db'],
|
570
|
+
username: params['user'],
|
571
|
+
password: params['password'],
|
572
|
+
driver: 'QPSQL',
|
573
|
+
}
|
574
|
+
|
575
|
+
if params.has_key? 'port'
|
576
|
+
vars[:port] = params['port']
|
577
|
+
end
|
578
|
+
|
579
|
+
db = JW::DBI::Database.new()
|
580
|
+
|
581
|
+
if db.open(vars) == false
|
582
|
+
raise "Failed to connect: #{db.error()}"
|
583
|
+
end
|
584
|
+
|
585
|
+
return db
|
586
|
+
end
|
587
|
+
|
588
|
+
end # class Context
|
589
|
+
|
590
|
+
end # module HareDo
|
591
|
+
end # module Sonar
|
592
|
+
```
|
593
|
+
|
594
|
+
This uses my custom database abstraction extensions which I want to add to
|
595
|
+
<tt>HareDo::Daemon</tt>. To incorporate it, I simply add it to the modules
|
596
|
+
section on the config file. The daemon will load this on startup, the module
|
597
|
+
will add the custom method, and now my plugins defined in the <tt>services</tt>
|
598
|
+
section of the configuration file will have it available to them on service
|
599
|
+
load. For example:
|
600
|
+
|
601
|
+
```ruby
|
602
|
+
require 'haredo/plugin'
|
603
|
+
|
604
|
+
class Plugin < HareDo::Plugin
|
605
|
+
|
606
|
+
UUID = 'log'
|
607
|
+
|
608
|
+
attr_reader :db, :logrecord
|
609
|
+
|
610
|
+
def initialize(peer, config)
|
611
|
+
super UUID, peer, config
|
612
|
+
|
613
|
+
@db = @peer.connectDB(config['db'])
|
614
|
+
end
|
615
|
+
|
616
|
+
def finalize()
|
617
|
+
super
|
618
|
+
|
619
|
+
@db.close()
|
620
|
+
end
|
621
|
+
|
622
|
+
.
|
623
|
+
.
|
624
|
+
.
|
625
|
+
end
|
626
|
+
```
|
627
|
+
|
628
|
+
The <tt>peer</tt> member is a reference to the <tt>Daemon</tt> instance which
|
629
|
+
has been modified to include the <tt>connectDB()</tt> method.
|
630
|
+
|
631
|
+
This gives you an easy way to customize the daemon without having to derive a
|
632
|
+
new class and instantiate it (and thus having to create a new binary and/or
|
633
|
+
modify the command-line utility).
|
634
|
+
|
635
|
+
### Command Line
|
636
|
+
|
637
|
+
The command line utility is simple. It includes <tt>start</tt>, <tt>stop</tt>
|
638
|
+
and <tt>status</tt> commands. It also has a help (<tt>-h</tt>) switch, along
|
639
|
+
with each command. So you can check the status using the <tt>haredo status</tt>
|
640
|
+
command which detects if the daemon is running. If it is, it sends it a message
|
641
|
+
and get back basic information in JSON, including loaded plugins:
|
642
|
+
|
643
|
+
```bash
|
644
|
+
bash $ haredo status
|
645
|
+
{"time":"2013-10-01 12:12:02 -0500","pid":"25290","plugins":["echo","dbi","log","status"]}
|
646
|
+
```
|
647
|
+
|
648
|
+
There is also a subcommand called <tt>config</tt>. This is used to test and set
|
649
|
+
values in the configuration file for broker and (otionally) a database. It
|
650
|
+
exists mainly as a utility for installers. You can test a broker connection as
|
651
|
+
follows:
|
652
|
+
|
653
|
+
```bash
|
654
|
+
bash $ PRBPASSWORD=guest haredo config broker -t -u guest -s localhost
|
655
|
+
{:user=>"guest", :password=>nil, :port=>nil, :vhost=>nil, :host=>"localhost", :ssl=>false}
|
656
|
+
succeeded
|
657
|
+
```
|
658
|
+
|
659
|
+
The RabbitMQ user password is passed in via environmental variables.
|
660
|
+
|
661
|
+
Most likely you will never need to use this as it was designed specifically for
|
662
|
+
use in the Debian installer.
|
663
|
+
|
325
664
|
## Installation
|
326
665
|
|
327
666
|
You can install directly from the command line via Ruby gems as follows:
|