tinyq 0.5.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.
- data/bin/tinyq +8 -0
- data/lib/tinyq.rb +37 -0
- data/lib/tinyq/bucket.rb +138 -0
- data/lib/tinyq/client.rb +59 -0
- data/lib/tinyq/connection.rb +160 -0
- data/lib/tinyq/defs.rb +4 -0
- data/lib/tinyq/funnel.rb +91 -0
- data/lib/tinyq/journal.rb +21 -0
- data/lib/tinyq/permanent.rb +33 -0
- data/lib/tinyq/server.rb +106 -0
- data/lib/tinyq/subscriber.rb +63 -0
- data/lib/tinyq/version.rb +3 -0
- metadata +114 -0
data/bin/tinyq
ADDED
data/lib/tinyq.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# Install
|
2
|
+
# gem install eventmachine json uuid
|
3
|
+
#
|
4
|
+
#
|
5
|
+
# TODO
|
6
|
+
# capped - for bucket and funnel
|
7
|
+
# - overflow or round-robin
|
8
|
+
#
|
9
|
+
# permanent - for bucket and funnel
|
10
|
+
# stream - for GetMessages
|
11
|
+
# (basically if count = 0 then send 1 message at a time, but never remove subscriber
|
12
|
+
|
13
|
+
require 'rubygems'
|
14
|
+
require 'eventmachine'
|
15
|
+
require 'json'
|
16
|
+
require 'logger'
|
17
|
+
require 'time'
|
18
|
+
require 'uuid'
|
19
|
+
require 'getopt/std'
|
20
|
+
require 'singleton'
|
21
|
+
require 'daemons'
|
22
|
+
|
23
|
+
require 'tinyq/subscriber'
|
24
|
+
require 'tinyq/funnel'
|
25
|
+
require 'tinyq/bucket'
|
26
|
+
require 'tinyq/server'
|
27
|
+
require 'tinyq/connection'
|
28
|
+
require 'tinyq/permanent'
|
29
|
+
require 'tinyq/client'
|
30
|
+
require 'tinyq/version'
|
31
|
+
require 'tinyq/defs'
|
32
|
+
require 'tinyq/journal'
|
33
|
+
|
34
|
+
|
35
|
+
$LOG = Logger.new(STDOUT)
|
36
|
+
$LOG.level = Logger::INFO
|
37
|
+
|
data/lib/tinyq/bucket.rb
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
module TinyQ
|
2
|
+
#
|
3
|
+
# Bucket, messages get dropped into the bucket and forwarded
|
4
|
+
# to the different funnels connected to that bucket
|
5
|
+
#
|
6
|
+
class Bucket
|
7
|
+
attr_accessor :name
|
8
|
+
|
9
|
+
attr_reader :messages
|
10
|
+
attr_reader :message_ids
|
11
|
+
attr_reader :references
|
12
|
+
attr_reader :pendings
|
13
|
+
|
14
|
+
attr_accessor :funnels
|
15
|
+
|
16
|
+
attr_accessor :permanent
|
17
|
+
|
18
|
+
def initialize(n, p = false)
|
19
|
+
@name = n
|
20
|
+
@permanent = p
|
21
|
+
@messages = {}
|
22
|
+
@message_ids = []
|
23
|
+
@references = {}
|
24
|
+
@pendings = {}
|
25
|
+
@funnels = {}
|
26
|
+
@uuid = UUID.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def put_message(message)
|
30
|
+
message_id = @uuid.generate
|
31
|
+
#message[:__id] = message_id
|
32
|
+
#message[:__sent] = Time.now.iso8601
|
33
|
+
|
34
|
+
# If permantent bucket, then store
|
35
|
+
Permanent.store message,"#{message_id}.dat", { :gzip => true } unless !@permanent
|
36
|
+
|
37
|
+
@messages[message_id] = message
|
38
|
+
@message_ids.push(message_id)
|
39
|
+
@references[message_id] = @funnels.keys
|
40
|
+
@pendings[message_id] = []
|
41
|
+
|
42
|
+
if !@funnels.empty?
|
43
|
+
# Put message in each funnel
|
44
|
+
@funnels.each do |n,funnel|
|
45
|
+
$LOG.debug("Bucket #{@name} - Notifying funnel #{funnel.name}")
|
46
|
+
funnel.notify(self)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def dequeue(funnel)
|
52
|
+
if !@messages.empty?
|
53
|
+
message_id = nil
|
54
|
+
|
55
|
+
@message_ids.each do |mid|
|
56
|
+
$LOG.debug("Bucket #{@name} - #{mid} references: #{@references[mid]}")
|
57
|
+
if @references[mid].count(funnel.name) > 0
|
58
|
+
# That message was not de-referenced yet
|
59
|
+
$LOG.debug("Bucket #{@name} - #{mid} -> #{funnel.name}")
|
60
|
+
message_id = mid
|
61
|
+
break
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
if nil != message_id
|
66
|
+
$LOG.debug("Bucket #{@name} - Sending #{message_id} to funnel #{funnel.name}")
|
67
|
+
message = @messages[message_id]
|
68
|
+
|
69
|
+
# Remove the given funnel from a reference
|
70
|
+
@references[message_id].delete(funnel.name)
|
71
|
+
# Add the given funnel to the pending list for that message
|
72
|
+
@pendings[message_id].push(funnel.name)
|
73
|
+
|
74
|
+
[message, message_id]
|
75
|
+
else
|
76
|
+
$LOG.debug("Bucket #{@name} - No more messages for funnel #{funnel.name}")
|
77
|
+
[nil, nil]
|
78
|
+
end
|
79
|
+
else
|
80
|
+
[nil, nil]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def message_sent(funnel, message_id)
|
85
|
+
$LOG.debug("Bucket #{@name} - Message #{message_id} sent on #{funnel.name}")
|
86
|
+
@pendings[message_id].delete(funnel.name)
|
87
|
+
|
88
|
+
# If no funnels are either pending or referenced, then message can be removed
|
89
|
+
if @pendings[message_id].empty? && @references[message_id].empty?
|
90
|
+
$LOG.debug("Bucket #{@name} - Purge message #{message_id}")
|
91
|
+
# No more references, message can be deleted
|
92
|
+
Permanent.remove "#{message_id}.dat" unless !@permanent
|
93
|
+
@messages.delete(message_id)
|
94
|
+
@message_ids.delete(message_id)
|
95
|
+
@references.delete(message_id)
|
96
|
+
@pendings.delete(message_id)
|
97
|
+
true
|
98
|
+
end
|
99
|
+
|
100
|
+
false
|
101
|
+
end
|
102
|
+
|
103
|
+
def funnel(name, broadcaster = false)
|
104
|
+
funnel = @funnels[name]
|
105
|
+
if nil == funnel
|
106
|
+
$LOG.info("Bucket #{@name} - Creating funnel #{name}")
|
107
|
+
funnel = Funnel.new(name, broadcaster)
|
108
|
+
feed_funnel(funnel)
|
109
|
+
end
|
110
|
+
# Update potential settings
|
111
|
+
funnel.broadcaster = broadcaster
|
112
|
+
|
113
|
+
funnel
|
114
|
+
end
|
115
|
+
|
116
|
+
def feed_funnel(funnel)
|
117
|
+
@funnels[funnel.name] = funnel
|
118
|
+
# Add ourself to the list of buckets for that funnel
|
119
|
+
if funnel.buckets.count self == 0
|
120
|
+
funnel.buckets.push(self)
|
121
|
+
end
|
122
|
+
|
123
|
+
# If we have cached messages, notify new funnel
|
124
|
+
if !@messages.empty?
|
125
|
+
# Add the current funnels as a reference
|
126
|
+
@message_ids.each do |mid|
|
127
|
+
@references[mid].push(funnel.name)
|
128
|
+
end
|
129
|
+
$LOG.debug("Bucket #{@name} - Notifying funnel #{funnel.name}")
|
130
|
+
funnel.notify(self)
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
|
data/lib/tinyq/client.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
module TinyQ
|
2
|
+
class ClientConnection < EventMachine::Connection
|
3
|
+
include EventMachine::Protocols::LineText2
|
4
|
+
|
5
|
+
attr_accessor :client
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
set_delimiter TinyQ::DELIMITER
|
9
|
+
end
|
10
|
+
|
11
|
+
def receive_line message
|
12
|
+
begin
|
13
|
+
reply = JSON.parse(message)
|
14
|
+
@client.callbacks.each { |c| c.call(reply) }
|
15
|
+
rescue Exception => e
|
16
|
+
puts e
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
class Client
|
23
|
+
attr_accessor :callbacks
|
24
|
+
|
25
|
+
def initialize
|
26
|
+
@callbacks = []
|
27
|
+
end
|
28
|
+
|
29
|
+
def connect(host, port)
|
30
|
+
@host, @port = host, port
|
31
|
+
@connection = EM.connect @host, @port, TinyQ::ClientConnection
|
32
|
+
@connection.client = self
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Public API
|
37
|
+
#
|
38
|
+
def put_message(bucket, message)
|
39
|
+
command = {:Command => "PutMessages", :Bucket => bucket, :Message => message}
|
40
|
+
@connection.send_data("#{command.to_json}#{TinyQ::DELIMITER}")
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_message(funnel)
|
44
|
+
command = {:Command => "GetMessages", :Funnel => funnel}
|
45
|
+
@connection.send_data("#{command.to_json}#{TinyQ::DELIMITER}")
|
46
|
+
end
|
47
|
+
|
48
|
+
def onreply(&block)
|
49
|
+
@callbacks << block
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
def feed_funnel(bucket, funnel)
|
54
|
+
command = {:Command => "FeedFunnel", :Bucket => bucket, :Funnel => funnel}
|
55
|
+
@connection.send_data("#{command.to_json}#{TinyQ::DELIMITER}")
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
module TinyQ
|
2
|
+
class Connection < EventMachine::Connection
|
3
|
+
attr_accessor :server
|
4
|
+
attr_reader :ip
|
5
|
+
attr_reader :port
|
6
|
+
|
7
|
+
include EventMachine::Protocols::LineText2
|
8
|
+
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
set_delimiter TinyQ::DELIMITER
|
12
|
+
end
|
13
|
+
|
14
|
+
def post_init
|
15
|
+
@port, *ip_parts = get_peername[2,6].unpack "nC4"
|
16
|
+
@ip = ip_parts.join('.')
|
17
|
+
|
18
|
+
$LOG.info("#{@ip}:#{@port} connected")
|
19
|
+
end
|
20
|
+
|
21
|
+
def receive_line line
|
22
|
+
process_message line
|
23
|
+
end
|
24
|
+
|
25
|
+
def reply rsp
|
26
|
+
send_data("#{rsp.to_json}#{TinyQ::DELIMITER}")
|
27
|
+
end
|
28
|
+
|
29
|
+
def ok
|
30
|
+
response = {:Status => "OK"}
|
31
|
+
reply(response)
|
32
|
+
end
|
33
|
+
|
34
|
+
def failed errmsg
|
35
|
+
response = {:Status => "Failed", :Message => errmsg}
|
36
|
+
reply(response)
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
def process_message data
|
41
|
+
begin
|
42
|
+
request = JSON.parse(data)
|
43
|
+
$LOG.debug("Request: #{request}")
|
44
|
+
if request.has_key? 'Command'
|
45
|
+
$LOG.debug("Command - #{request['Command']}")
|
46
|
+
|
47
|
+
case request['Command']
|
48
|
+
when "PutMessages"
|
49
|
+
# Put message in a bucket
|
50
|
+
if request.has_key? 'Bucket'
|
51
|
+
bucket = @server.bucket(request['Bucket'])
|
52
|
+
|
53
|
+
if request['Message'].kind_of?(Array)
|
54
|
+
request['Message'].each do |message|
|
55
|
+
Journal.instance.event({:Event=>"PutMessage"})
|
56
|
+
bucket.put_message(message)
|
57
|
+
end
|
58
|
+
else
|
59
|
+
Journal.instance.event({:Event=>"PutMessage"})
|
60
|
+
bucket.put_message(request['Message'])
|
61
|
+
end
|
62
|
+
|
63
|
+
ok()
|
64
|
+
else
|
65
|
+
failed("Parameter Bucket missing")
|
66
|
+
end
|
67
|
+
when "FeedFunnel"
|
68
|
+
# Feed Bucket to Funnel
|
69
|
+
if request.has_key? 'Bucket' and request.has_key? 'Funnel'
|
70
|
+
# Hook up a funnel to a bucket
|
71
|
+
bucket = @server.bucket(request['Bucket'])
|
72
|
+
funnel = @server.funnels[request['Funnel']]
|
73
|
+
if nil != funnel
|
74
|
+
bucket.feed_funnel(funnel)
|
75
|
+
else
|
76
|
+
if request.has_key? 'Broadcaster'
|
77
|
+
funnel = bucket.funnel(request['Funnel'], request['Broadcaster'])
|
78
|
+
else
|
79
|
+
funnel = bucket.funnel(request['Funnel'])
|
80
|
+
end
|
81
|
+
@server.funnels[request['Funnel']] = funnel
|
82
|
+
end
|
83
|
+
|
84
|
+
ok()
|
85
|
+
else
|
86
|
+
failed("Parameter Bucket or Funnel missing")
|
87
|
+
end
|
88
|
+
when "GetMessages"
|
89
|
+
# Get Messages from Funnel
|
90
|
+
if request.has_key? 'Funnel'
|
91
|
+
funnel = @server.funnels[request['Funnel']]
|
92
|
+
if nil == funnel
|
93
|
+
$LOG.warn("Funnel was never fed...")
|
94
|
+
failed("Funnel was never fed")
|
95
|
+
else
|
96
|
+
if request.has_key? 'Ack'
|
97
|
+
ok()
|
98
|
+
end
|
99
|
+
|
100
|
+
if request.has_key? 'Count'
|
101
|
+
funnel.add_connection(self, request['Count'])
|
102
|
+
else
|
103
|
+
funnel.add_connection(self)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
else
|
107
|
+
failed("Parameter Funnel missing")
|
108
|
+
end
|
109
|
+
when "GetInfo"
|
110
|
+
if request.has_key? 'Bucket'
|
111
|
+
bucket = @server.bucket(request['Bucket'])
|
112
|
+
|
113
|
+
response = {
|
114
|
+
:Status => 'OK',
|
115
|
+
:Bucket => {
|
116
|
+
:Name => bucket.name,
|
117
|
+
:MessageCount => bucket.messages.size,
|
118
|
+
:Funnels => [],
|
119
|
+
}
|
120
|
+
}
|
121
|
+
|
122
|
+
bucket.funnels.each do |n,funnel|
|
123
|
+
response[:Bucket][:Funnels].push({
|
124
|
+
:Name => funnel.name,
|
125
|
+
:MessageCount => funnel.messages.size,
|
126
|
+
:SubscriberCount => funnel.subscribers.size,
|
127
|
+
})
|
128
|
+
end
|
129
|
+
|
130
|
+
reply(response)
|
131
|
+
|
132
|
+
elsif request.has_key? 'Funnel'
|
133
|
+
else
|
134
|
+
failed("Parameter Funnel or Bucket missing")
|
135
|
+
end
|
136
|
+
else
|
137
|
+
# Unsupported command
|
138
|
+
failed("Unsupported command")
|
139
|
+
end
|
140
|
+
|
141
|
+
else
|
142
|
+
# Did not understand
|
143
|
+
failed("Parameter Command missing")
|
144
|
+
end
|
145
|
+
rescue Exception => e
|
146
|
+
$LOG.error("Failed #{e}")
|
147
|
+
failed("Internal error #{e}")
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def unbind
|
152
|
+
$LOG.info("#{@ip}:#{@port} disconnected")
|
153
|
+
|
154
|
+
@server.remove_connection(self)
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
|
data/lib/tinyq/defs.rb
ADDED
data/lib/tinyq/funnel.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
module TinyQ
|
2
|
+
#
|
3
|
+
# Funnel will receive messages on the internal queue and will dispatch
|
4
|
+
# to subscribers
|
5
|
+
#
|
6
|
+
class Funnel
|
7
|
+
attr_accessor :name
|
8
|
+
attr_reader :queue
|
9
|
+
attr_accessor :buckets
|
10
|
+
attr_accessor :subscribers
|
11
|
+
attr_accessor :broadcaster
|
12
|
+
|
13
|
+
def initialize(n, b = false)
|
14
|
+
@name = n
|
15
|
+
@broadcaster = b
|
16
|
+
@queue = EventMachine::Queue.new
|
17
|
+
@subscribers = {}
|
18
|
+
@buckets = []
|
19
|
+
|
20
|
+
cb = Proc.new do |event|
|
21
|
+
$LOG.debug("Funnel #{@name} - Callback")
|
22
|
+
if !@subscribers.empty?
|
23
|
+
# OK we can dequeue from bucket since we have somewhere to send it
|
24
|
+
bucket = event[:Bucket]
|
25
|
+
message,message_id = bucket.dequeue(self)
|
26
|
+
|
27
|
+
$LOG.debug("Funnel #{@name} - Callback got '#{message_id}'")
|
28
|
+
|
29
|
+
if message != nil
|
30
|
+
if @broadcaster
|
31
|
+
$LOG.debug("Funnel #{@name} - Broadcasting #{message_id}")
|
32
|
+
@subscribers.each do |c,subscriber|
|
33
|
+
if subscriber.put_message(bucket, self, message, message_id)
|
34
|
+
# Subscriber is done removing
|
35
|
+
$LOG.debug("Funnel #{@name} - Subscriber received requested count")
|
36
|
+
self.remove_connection(c)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
else
|
40
|
+
$LOG.debug("Funnel #{@name} - Unicasting #{message_id}")
|
41
|
+
c = @subscribers.keys[0]
|
42
|
+
subscriber = @subscribers[c]
|
43
|
+
$LOG.debug("Funnel #{@name} - Unicasting to #{subscriber.connection.ip}:#{subscriber.connection.port}")
|
44
|
+
if subscriber.put_message(bucket, self, message, message_id)
|
45
|
+
# Subscriber is done removing
|
46
|
+
$LOG.debug("Funnel #{@name} - Subscriber #{subscriber.connection.ip}:#{subscriber.connection.port} received requested count")
|
47
|
+
self.remove_connection(c)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
else
|
51
|
+
$LOG.debug("Funnel #{@name} - Callback noop")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Wait for next event
|
56
|
+
@queue.pop &cb
|
57
|
+
end
|
58
|
+
|
59
|
+
@queue.pop &cb
|
60
|
+
end
|
61
|
+
|
62
|
+
# Method called by bucket when messages are available
|
63
|
+
def notify(bucket)
|
64
|
+
@queue.push({:Event => "New Message", :Bucket => bucket})
|
65
|
+
end
|
66
|
+
|
67
|
+
def add_connection(c,n = 1)
|
68
|
+
subscriber = @subscribers[c]
|
69
|
+
if nil == subscriber
|
70
|
+
subscriber = Subscriber.new(c,n)
|
71
|
+
@subscribers[c] = subscriber
|
72
|
+
end
|
73
|
+
|
74
|
+
# At this point, we need to see if any buckets we are connected to
|
75
|
+
# have pending messages
|
76
|
+
@buckets.each do |bucket|
|
77
|
+
if !bucket.messages.empty?
|
78
|
+
self.notify(bucket)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def remove_connection(c)
|
84
|
+
$LOG.debug("Funnel #{@name} - Removing subscriber #{c.ip}:#{c.port}")
|
85
|
+
@subscribers.delete(c)
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module TinyQ
|
2
|
+
class Journal
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
attr_accessor :enabled
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@journal = File.open("journal.txt", "a")
|
9
|
+
@enabled = false
|
10
|
+
end
|
11
|
+
|
12
|
+
def event(event)
|
13
|
+
if @enabled
|
14
|
+
now=Time.now.iso8601
|
15
|
+
@journal.puts("#{now},#{event[:Event]}")
|
16
|
+
@journal.fsync
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
|
3
|
+
module TinyQ
|
4
|
+
class Permanent
|
5
|
+
|
6
|
+
|
7
|
+
def self.store obj, filename, options = {}
|
8
|
+
dump = Marshal.dump(obj)
|
9
|
+
file = File.new(filename, 'w')
|
10
|
+
file = Zlib::GzipWriter.new(file) unless options[:gzip] == false
|
11
|
+
file.write dump
|
12
|
+
file.close
|
13
|
+
return obj
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.load filename
|
17
|
+
begin
|
18
|
+
file = Zlib::GzipReader.open(filename)
|
19
|
+
rescue Zlib::GzipFile::Error
|
20
|
+
file = File.open(filename, 'r')
|
21
|
+
ensure
|
22
|
+
obj = Marshal.load file.read
|
23
|
+
file.close
|
24
|
+
return obj
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.remove filename
|
29
|
+
File.delete(file)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
data/lib/tinyq/server.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
module TinyQ
|
2
|
+
class Server
|
3
|
+
attr_accessor :connections
|
4
|
+
|
5
|
+
attr_accessor :buckets
|
6
|
+
attr_accessor :funnels
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@connections = []
|
10
|
+
@buckets = {}
|
11
|
+
@funnels = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def usage
|
15
|
+
puts "#{$0}"
|
16
|
+
puts "version: #{TinyQ::VERSION}"
|
17
|
+
puts " -b go into background/daemonize"
|
18
|
+
puts " -p <port> port number to listen to (default: 64321)"
|
19
|
+
puts " -i <ip> ip to bind to (default: 127.0.0.1)"
|
20
|
+
puts " -d turn on debug"
|
21
|
+
puts " -h help, this message"
|
22
|
+
end
|
23
|
+
|
24
|
+
def main(args)
|
25
|
+
opt = Getopt::Std.getopts("bp:i:dh")
|
26
|
+
|
27
|
+
if opt['h']
|
28
|
+
usage
|
29
|
+
exit
|
30
|
+
end
|
31
|
+
|
32
|
+
@ip = opt['i'] || '127.0.0.1'
|
33
|
+
@port = opt['p'] || 64321
|
34
|
+
|
35
|
+
if opt['d']
|
36
|
+
$LOG.level = Logger::DEBUG
|
37
|
+
end
|
38
|
+
|
39
|
+
if opt['b']
|
40
|
+
puts "Going into background"
|
41
|
+
Daemons.daemonize
|
42
|
+
end
|
43
|
+
|
44
|
+
start
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.start
|
48
|
+
new().start
|
49
|
+
end
|
50
|
+
|
51
|
+
def start
|
52
|
+
EventMachine::run {
|
53
|
+
trap("TERM") { stop }
|
54
|
+
trap("INT") { stop }
|
55
|
+
EventMachine.epoll
|
56
|
+
@signature = EventMachine.start_server(@ip, @port, Connection) do |con|
|
57
|
+
con.server = self
|
58
|
+
# We actually do not want to wait on clients
|
59
|
+
#@connections.push(con)
|
60
|
+
end
|
61
|
+
$LOG.info("TinyQ listening on 0.0.0.0:64321")
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
def stop
|
67
|
+
$LOG.info("TinyQ Exiting")
|
68
|
+
EventMachine.stop_server(@signature)
|
69
|
+
|
70
|
+
unless wait_for_connections_and_stop
|
71
|
+
EventMachine.add_periodic_timer(1) { wait_for_connections_and_stop }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def wait_for_connections_and_stop
|
76
|
+
if @connections.empty?
|
77
|
+
EventMachine.stop
|
78
|
+
true
|
79
|
+
else
|
80
|
+
$LOG.info("Waiting for #{@connections.size} connection(s) to finish...")
|
81
|
+
false
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def bucket(name)
|
86
|
+
bucket = @buckets[name]
|
87
|
+
if nil == bucket
|
88
|
+
$LOG.info("Server - Creating bucket #{name}")
|
89
|
+
bucket = Bucket.new(name)
|
90
|
+
@buckets[name] = bucket
|
91
|
+
end
|
92
|
+
|
93
|
+
bucket
|
94
|
+
end
|
95
|
+
|
96
|
+
def remove_connection(c)
|
97
|
+
@connections.delete(c)
|
98
|
+
@funnels.each do |name,funnel|
|
99
|
+
funnel.remove_connection(c)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module TinyQ
|
2
|
+
class Subscriber
|
3
|
+
attr_accessor :connection
|
4
|
+
attr_reader :queue
|
5
|
+
attr_reader :requested
|
6
|
+
attr_accessor :count
|
7
|
+
attr_reader :messages
|
8
|
+
attr_reader :message_ids
|
9
|
+
|
10
|
+
def initialize(c,n = 1)
|
11
|
+
@connection = c
|
12
|
+
@requested = @count = n
|
13
|
+
@queue = EventMachine::Queue.new
|
14
|
+
@messages = []
|
15
|
+
@message_ids = {}
|
16
|
+
|
17
|
+
cb = Proc.new do |event|
|
18
|
+
$LOG.debug("Subscriber #{@connection.ip}:#{@connection.port} - Queue callback")
|
19
|
+
|
20
|
+
response = { :Messages => @messages }
|
21
|
+
|
22
|
+
connection.reply(response)
|
23
|
+
|
24
|
+
# Only at that point should the message be removed from the bucket!
|
25
|
+
@message_ids.each do |message_id,info|
|
26
|
+
bucket = info[:Bucket]
|
27
|
+
funnel = info[:Funnel]
|
28
|
+
|
29
|
+
bucket.message_sent(funnel, message_id)
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
@queue.pop &cb
|
34
|
+
end
|
35
|
+
|
36
|
+
@queue.pop &cb
|
37
|
+
end
|
38
|
+
|
39
|
+
def put_message(bucket, funnel, message, message_id)
|
40
|
+
@messages.push(message)
|
41
|
+
@message_ids[message_id] = {
|
42
|
+
:Funnel => funnel,
|
43
|
+
:Bucket => bucket
|
44
|
+
}
|
45
|
+
|
46
|
+
if @messages.size == @requested
|
47
|
+
# We have everything now, so we can send all messages to subscriber
|
48
|
+
event = {
|
49
|
+
:Event => "New Message",
|
50
|
+
:Funnel => funnel,
|
51
|
+
:Bucket => bucket,
|
52
|
+
:MessageID => message_id
|
53
|
+
}
|
54
|
+
@queue.push(event)
|
55
|
+
true
|
56
|
+
else
|
57
|
+
false
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
|
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tinyq
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jerome Poichet
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-10-15 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: eventmachine
|
16
|
+
requirement: &70364366145240 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70364366145240
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: json
|
27
|
+
requirement: &70364366144820 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70364366144820
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: uuid
|
38
|
+
requirement: &70364366144400 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70364366144400
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: getopt
|
49
|
+
requirement: &70364366172140 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70364366172140
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: daemons
|
60
|
+
requirement: &70364366171720 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :runtime
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70364366171720
|
69
|
+
description: TinyQ is a message broker with a simple JSON based protocol
|
70
|
+
email:
|
71
|
+
- poitch@gmail.com
|
72
|
+
executables:
|
73
|
+
- tinyq
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- lib/tinyq.rb
|
78
|
+
- lib/tinyq/bucket.rb
|
79
|
+
- lib/tinyq/client.rb
|
80
|
+
- lib/tinyq/connection.rb
|
81
|
+
- lib/tinyq/defs.rb
|
82
|
+
- lib/tinyq/funnel.rb
|
83
|
+
- lib/tinyq/journal.rb
|
84
|
+
- lib/tinyq/permanent.rb
|
85
|
+
- lib/tinyq/server.rb
|
86
|
+
- lib/tinyq/subscriber.rb
|
87
|
+
- lib/tinyq/version.rb
|
88
|
+
- bin/tinyq
|
89
|
+
homepage: https://github.com/poitch/TinyQ
|
90
|
+
licenses: []
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options: []
|
93
|
+
require_paths:
|
94
|
+
- lib
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
none: false
|
104
|
+
requirements:
|
105
|
+
- - ! '>='
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
requirements: []
|
109
|
+
rubyforge_project:
|
110
|
+
rubygems_version: 1.8.6
|
111
|
+
signing_key:
|
112
|
+
specification_version: 3
|
113
|
+
summary: Ruby/TinyQ
|
114
|
+
test_files: []
|