message_channel 1.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.
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "message_channel"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,18 @@
1
+ require "message_channel/version"
2
+ require "message_channel/base"
3
+
4
+ module MessageChannel
5
+
6
+ class Error < StandardError; end
7
+
8
+ class << self
9
+ include MessageChannel::Base
10
+ end
11
+
12
+ autoload :Observer, "message_channel/observer"
13
+ autoload :Druby, "message_channel/druby"
14
+ autoload :Mqtt, "message_channel/mqtt"
15
+ autoload :Redis, "message_channel/redis"
16
+ autoload :Mongodb, "message_channel/mongodb"
17
+ end
18
+
@@ -0,0 +1,54 @@
1
+ module MessageChannel
2
+ class Error < StandardError; end
3
+
4
+ module Base
5
+
6
+ # uri:
7
+ # "observer"
8
+ # "druby://127.0.0.1:8787"
9
+ # "mqtt://127.0.0.1:1883"
10
+ # "redis://127.0.0.1:6379/0"
11
+ # "mongodb://127.0.0.1:27017/test?size=4000&name=_event_queue"
12
+
13
+ def new( uri = nil, type: nil, host: nil, port: nil, db: nil, size: nil, name: nil )
14
+ if uri
15
+ uris = URI.parse( uri )
16
+ if uris.scheme.nil? && uris.host.nil? && uris.port.nil? && uris.path
17
+ type = uris.path
18
+ params = []
19
+ else
20
+ type = uris.scheme || type
21
+ host = uris.host || host
22
+ port = uris.port || port
23
+ params = uris.path.gsub(/^\//, "").split('/')
24
+ query = Hash[ URI::decode_www_form(uris.query) ] rescue {}
25
+ size = query[:size] || size
26
+ name = query[:name] || name
27
+ end
28
+ else
29
+ parans = []
30
+ end
31
+
32
+ options = { host: host, port: port }
33
+
34
+ case type
35
+ when "observer", NilClass
36
+ MessageChannel::Observer.new( **options )
37
+ when "druby"
38
+ MessageChannel::Druby.new( **options )
39
+ when "mqtt"
40
+ MessageChannel::Mqtt.new( **options )
41
+ when "redis"
42
+ options[:db] = params.shift || db
43
+ MessageChannel::Redis.new( **options )
44
+ when "mongodb"
45
+ options[:db] = params.shift || db
46
+ options[:size] = size
47
+ options[:name] = name
48
+ MessageChannel::Mongodb.new( **options )
49
+ end
50
+ end
51
+
52
+ end
53
+ end
54
+
@@ -0,0 +1,182 @@
1
+ require "drb/drb"
2
+ require "json"
3
+
4
+ module MessageChannel
5
+
6
+ class Druby
7
+
8
+ class Broker
9
+ def initialize
10
+ @mutex = Mutex.new
11
+ @patterns = {}
12
+ end
13
+
14
+ def subscribe( pattern )
15
+ queue = Queue.new
16
+ queue_id = queue.object_id
17
+ @mutex.synchronize do
18
+ @patterns[queue_id] = [pattern, queue]
19
+ end
20
+ queue_id
21
+ end
22
+
23
+ def unsubscribe( queue_id )
24
+ @mutex.synchronize do
25
+ @patterns.delete( queue_id ) rescue nil
26
+ end
27
+ rescue
28
+ nil
29
+ end
30
+
31
+ def wait( queue_id )
32
+ @patterns[queue_id].last.pop
33
+ end
34
+
35
+ def fetch( pattern )
36
+ queue = Queue.new
37
+ queue_id = queue.object_id
38
+ @mutex.synchronize do
39
+ @patterns[queue_id] = [pattern, queue]
40
+ end
41
+ topic, items = * queue.pop
42
+ rescue
43
+ nil
44
+ ensure
45
+ @mutex.synchronize do
46
+ @patterns.delete( queue_id ) rescue nil
47
+ end
48
+ end
49
+
50
+ def publish( topic, message )
51
+ @mutex.synchronize do
52
+ @patterns.each do |queue_id, items|
53
+ pattern, queue = *items
54
+ if File.fnmatch( pattern, topic, File::FNM_PATHNAME )
55
+ queue.push( [topic, message] )
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ class Agent
63
+ def initialize( host: @@host, port: @@port )
64
+ @@host = host
65
+ @@port = port
66
+ @uri = "druby://#{host}:#{port}"
67
+
68
+ if !defined?( @@Broker ) || @@Broker.nil?
69
+ @@Broker = Broker.new
70
+ DRb.start_service( @uri, @@Broker ) rescue nil
71
+ end
72
+
73
+ @drb = DRbObject.new_with_uri( @uri )
74
+ @queue_ids = {}
75
+ end
76
+
77
+ def listen_once( pattern )
78
+ topic, message = * @drb.fetch( pattern )
79
+ end
80
+
81
+ def listen_each( pattern, &block )
82
+ queue_id = @drb.subscribe( pattern )
83
+ @queue_ids[pattern] = queue_id
84
+ while true
85
+ topic, message = * @drb.wait( queue_id )
86
+ break if topic.nil?
87
+ block.call( topic, message )
88
+ end
89
+ rescue => error
90
+ nil
91
+ ensure
92
+ @drb.unsubscribe( queue_id )
93
+ @queue_ids.delete( pattern ) rescue nil
94
+ end
95
+
96
+ def unlisten( pattern )
97
+ if queue_id = @queue_ids[pattern]
98
+ @drb.unsubscribe( queue_id )
99
+ @queue_ids.delete( pattern ) rescue nil
100
+ end
101
+ end
102
+
103
+ def notify( topic, message )
104
+ @drb.publish( topic, message )
105
+ end
106
+ end
107
+
108
+ attr_reader :host, :port
109
+
110
+ def initialize( host: nil, port: nil )
111
+ @host = host || "127.0.0.1"
112
+ @port = ( port || 8787 ).to_i
113
+ @agent = Agent.new( host: @host, port: @port )
114
+ @threads = {}
115
+ end
116
+
117
+ def listen_once( *patterns )
118
+ queue = Queue.new
119
+ threads = {}
120
+ patterns.each do |pattern|
121
+ threads[pattern] = Thread.start(pattern) do |pattern|
122
+ agent = Agent.new
123
+ begin
124
+ topic, message = * agent.listen_once( pattern )
125
+ items = JSON.parse( message, symbolize_names: true )
126
+ queue.push( [topic, items] )
127
+ rescue => error
128
+ nil
129
+ end
130
+ end
131
+ end
132
+
133
+ topic, items = queue.pop
134
+ patterns.each do |pattern|
135
+ threads[pattern].kill rescue nil
136
+ threads.delete( pattern ) rescue nil
137
+ end
138
+ [topic, items]
139
+ rescue
140
+ nil
141
+ end
142
+
143
+ def listen_each( *patterns, &block )
144
+ patterns.each do |pattern|
145
+ @threads[pattern] = Thread.start(pattern) do |pattern|
146
+ begin
147
+ @agent.listen_each( pattern ) do |topic, message|
148
+ items = JSON.parse( message, symbolize_names: true )
149
+ block.call( topic, items )
150
+ end
151
+ rescue => error
152
+ nil
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ def listen( *patterns, &block )
159
+ if block.nil?
160
+ listen_once( *patterns )
161
+ else
162
+ listen_each( *patterns ) do |topic, items|
163
+ block.call( topic, items )
164
+ end
165
+ end
166
+ end
167
+
168
+ def unlisten( **patterns )
169
+ patterns.each do |pattern|
170
+ @agent.unlisten( pattern )
171
+ @threads.delete( pattern )
172
+ end
173
+ end
174
+
175
+ def notify( topic, **items )
176
+ @agent.notify( topic, items.to_json )
177
+ end
178
+
179
+ end
180
+
181
+ end
182
+
@@ -0,0 +1,131 @@
1
+ require "mongo"
2
+ require "json"
3
+
4
+ ::Mongo::Logger.logger.level = Logger::ERROR
5
+
6
+ module MessageChannel
7
+ class Mongodb
8
+ SIZE = 8000
9
+ NAME = "_event_queue"
10
+
11
+ def initialize( host: nil, port: nil, db: nil, size: nil, name: nil )
12
+ @host = host || "127.0.0.1"
13
+ @port = ( port || 27017 ).to_i
14
+ @db = db || "test"
15
+ @size = ( size || SIZE ).to_i
16
+ @name = name || NAME
17
+
18
+ @url = "mongodb://#{ @host }:#{ @port }/#{ @db }"
19
+ @client = ::Mongo::Client.new( @url )
20
+
21
+ @threads = {}
22
+ @mutex = Mutex.new
23
+
24
+ @event_queue = get_event_queue
25
+ end
26
+
27
+ def get_event_queue
28
+ event_queue = @mutex.synchronize do
29
+ if @client.database.collection_names.include?( @name )
30
+ event_queue = @client[ @name ]
31
+ else
32
+ event_queue = @client[ @name, capped: true, size: @size ]
33
+ event_queue.create
34
+ now = Time.now
35
+ doc = {
36
+ topic: "reset",
37
+ at: now.strftime("%Y%m%d.%H%M%S.%6L"),
38
+ }
39
+ event_queue.insert_one( doc )
40
+ event_queue
41
+ end
42
+ end
43
+ end
44
+
45
+ def get_event_tail( event_queue )
46
+ filter = {}
47
+ if enum = event_queue.find( {}, { sort: { "$natural" => -1 } } ).to_enum
48
+ if doc = enum.next rescue nil
49
+ filter = { "_id"=>{ "$gt"=>doc["_id"] } }
50
+ end
51
+ end
52
+ event_tail = event_queue.find( filter, { cursor_type: :tailable_await } ).to_enum
53
+ end
54
+
55
+ def listen_once( *patterns )
56
+ queue = Queue.new
57
+ threads = {}
58
+ patterns.each do |pattern|
59
+ threads[pattern] = ::Thread.start(pattern) do |pattern|
60
+ event_queue = get_event_queue
61
+ event_tail = get_event_tail( event_queue )
62
+ begin
63
+ while doc = event_tail.next
64
+ items = JSON.parse( doc.to_json, symbolize_names: true )
65
+ topic = items[:topic]
66
+ if File.fnmatch( pattern, topic, File::FNM_PATHNAME )
67
+ items.delete( :_id )
68
+ items.delete( :topic )
69
+ queue.push [topic, items]
70
+ end
71
+ end
72
+ ensure
73
+ end
74
+ end
75
+ end
76
+
77
+ topic, items = queue.pop
78
+ patterns.each do |pattern|
79
+ threads[pattern].kill rescue nil
80
+ threads.delete( pattern ) rescue nil
81
+ end
82
+ [topic, items]
83
+ end
84
+
85
+ def listen_each( *patterns, &block )
86
+ patterns.each do |pattern|
87
+ @threads[pattern] = ::Thread.start(pattern) do |pattern|
88
+ begin
89
+ event_queue = get_event_queue
90
+ event_tail = get_event_tail( event_queue )
91
+ while doc = event_tail.next
92
+ items = JSON.parse( doc.to_json, symbolize_names: true )
93
+ topic = items[:topic]
94
+ if File.fnmatch( pattern, topic, File::FNM_PATHNAME )
95
+ items.delete( :_id )
96
+ items.delete( :topic )
97
+ block.call( topic, items )
98
+ end
99
+ end
100
+ ensure
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ def listen( *patterns, &block )
107
+ if block.nil?
108
+ listen_once( *patterns )
109
+ else
110
+ listen_each( *patterns ) do |topic, items|
111
+ block.call( topic, items )
112
+ end
113
+ end
114
+ end
115
+
116
+ def unlisten( **patterns )
117
+ patterns.each do |pattern|
118
+ @threads[pattern].kill rescue nil
119
+ @threads.delete( pattern ) rescue nil
120
+ end
121
+ end
122
+
123
+ def notify( topic, **items )
124
+ items[:topic] = topic
125
+ @event_queue.insert_one( items )
126
+ end
127
+
128
+ end
129
+
130
+ end
131
+
@@ -0,0 +1,79 @@
1
+ require "mqtt"
2
+ require "json"
3
+
4
+ module MessageChannel
5
+ class Mqtt
6
+
7
+ def initialize( host: nil, port: nil )
8
+ @host = host || "127.0.0.1"
9
+ @port = ( port || 1883 ).to_i
10
+ @mqtt = MQTT::Client.connect( @host, @port )
11
+ @threads = {}
12
+ end
13
+
14
+ def listen_once( *patterns )
15
+ queue = Queue.new
16
+ threads = {}
17
+ patterns.each do |pattern|
18
+ threads[pattern] = ::Thread.start(pattern) do |pattern|
19
+ mqtt = MQTT::Client.connect( @host, @port )
20
+ begin
21
+ mqtt.get( pattern ) do |topic, message|
22
+ items = JSON.parse( message, symbolize_names: true )
23
+ mqtt.disconnect rescue nil
24
+ queue.push [topic, items]
25
+ end
26
+ ensure
27
+ end
28
+ end
29
+ end
30
+
31
+ topic, items = queue.pop
32
+ patterns.each do |pattern|
33
+ threads[pattern].kill rescue nil
34
+ threads.delete( pattern ) rescue nil
35
+ end
36
+ [topic, items]
37
+ end
38
+
39
+ def listen_each( *patterns, &block )
40
+ patterns.each do |pattern|
41
+ @threads[pattern] = ::Thread.start(pattern) do |pattern|
42
+ mqtt = MQTT::Client.connect( @host, @port )
43
+ begin
44
+ mqtt.get( pattern ) do |topic, message|
45
+ items = JSON.parse( message, symbolize_names: true )
46
+ block.call( topic, items )
47
+ end
48
+ ensure
49
+ mqtt.disconnect rescue nil
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ def listen( *patterns, &block )
56
+ if block.nil?
57
+ listen_once( *patterns )
58
+ else
59
+ listen_each( *patterns ) do |topic, items|
60
+ block.call( topic, items )
61
+ end
62
+ end
63
+ end
64
+
65
+ def unlisten( **patterns )
66
+ patterns.each do |pattern|
67
+ @threads[pattern].kill rescue nil
68
+ @threads.delete( pattern ) rescue nil
69
+ end
70
+ end
71
+
72
+ def notify( topic, **items )
73
+ @mqtt.publish( topic, items.to_json, false )
74
+ end
75
+
76
+ end
77
+
78
+ end
79
+