celluloid-io-pg-listener 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +1 -1
- data/.travis.yml +6 -2
- data/Gemfile +1 -0
- data/README.md +101 -36
- data/Rakefile +19 -0
- data/bin/setup +19 -0
- data/bin/setup.sql +12 -0
- data/celluloid-io-pg-listener.gemspec +8 -3
- data/gemfiles/rails_3.2.22.gemfile.lock +3 -0
- data/gemfiles/rails_4.2.4.gemfile.lock +3 -0
- data/lib/celluloid-io-pg-listener.rb +7 -2
- data/lib/celluloid-io-pg-listener/client.rb +16 -7
- data/lib/celluloid-io-pg-listener/examples/client.rb +29 -0
- data/lib/celluloid-io-pg-listener/examples/double_super_example.rb +45 -0
- data/lib/celluloid-io-pg-listener/examples/listener_client_by_inheritance.rb +34 -0
- data/lib/celluloid-io-pg-listener/examples/notify_server_by_inheritance.rb +7 -0
- data/lib/celluloid-io-pg-listener/examples/server.rb +51 -0
- data/lib/celluloid-io-pg-listener/initialization/argument_extraction.rb +34 -0
- data/lib/celluloid-io-pg-listener/initialization/async_listener.rb +55 -0
- data/lib/celluloid-io-pg-listener/initialization/client_extracted_signature.rb +41 -0
- data/lib/celluloid-io-pg-listener/version.rb +1 -1
- metadata +77 -14
- data/lib/celluloid-io-pg-listener/listener.rb +0 -21
- data/lib/celluloid-io-pg-listener/server.rb +0 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1f0e16f28d6bd7ddda7c1cce2480269924c11690
|
4
|
+
data.tar.gz: 6d89f93dcdc3dea378d729e4d9adccb06e8427d0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c7e78c2385b61f25dcd7111460cd463f76b89feece98a31e27f5b7f82b3e5be097e0326dced44f2571f3da022203f072390d32e9cb3f3cd6a317cdd994ae76c7
|
7
|
+
data.tar.gz: 42ec7cf6cce2f8dbe724d1cb22b7f2e146f0eeca070479b1c602afe2d3031fe6f1d2c283f4a2b6fbd8cb013a2d68a9dcb0ddc53a61d707213212e5ef6e504eb5
|
data/.rspec
CHANGED
data/.travis.yml
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
language: ruby
|
2
|
+
cache: bundler
|
2
3
|
rvm:
|
3
|
-
- 2.1.
|
4
|
+
- 2.1.5
|
4
5
|
- 2.2.3
|
5
|
-
before_install:
|
6
|
+
before_install:
|
7
|
+
- gem install bundler -v 1.10.6
|
8
|
+
- bin/setup
|
9
|
+
script: "bundle exec rspec spec"
|
6
10
|
gemfile:
|
7
11
|
- gemfiles/rails_4.2.4.gemfile
|
8
12
|
- gemfiles/rails_3.2.22.gemfile
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
Simple way to NOTIFY and LISTEN to channels in PostgreSQL
|
4
4
|
|
5
|
+
The goal is to integrate the listener client with a Rails project, and that is underway in the spec/ folder, but as yet unfinished. Standalone the listener client works great.
|
6
|
+
|
5
7
|
Inspired by https://gist.github.com/tpitale/3915671
|
6
8
|
|
7
9
|
| Project | Celluloid IO PG Listener |
|
@@ -43,70 +45,134 @@ Find a data base that exists that you want to run notifications through. Won't
|
|
43
45
|
so doesn't matter which one you pick. Then pick an arbitrary name for the channel. Only requirement is that the server
|
44
46
|
and the client use the same database name and channel name or they won't be communicating.
|
45
47
|
|
46
|
-
In an irb session
|
48
|
+
In an irb session start the server, taking care to:
|
49
|
+
- replace the database names with your own
|
50
|
+
- replace the channel name, if you want, they are arbitrary, and don't need to be "created" in the DB.
|
47
51
|
|
48
52
|
```ruby
|
49
53
|
>> require "celluloid-io-pg-listener"
|
50
54
|
|
51
55
|
=> true
|
52
56
|
|
53
|
-
>>
|
57
|
+
>> $CELLULOID_DEBUG=true
|
58
|
+
=> true
|
54
59
|
|
55
|
-
|
60
|
+
>> server = CelluloidIOPGListener::Examples::Server.new(dbname: "celluloid_io_pg_listener_test", channel: "users_insert")
|
56
61
|
|
57
|
-
|
62
|
+
D, [2015-10-14T12:59:31.840206 #23209] DEBUG -- : Server will send notifications to celluloid_io_pg_listener_test:users_insert
|
58
63
|
|
59
|
-
|
64
|
+
=> #<Celluloid::Proxy::Cell(CelluloidIOPGListener::Examples::Server:0x3ff71a6f6db8) @client_extracted_signature=#<CelluloidIOPGListener::Initialization::ClientExtractedSignature:0x007fee34dec310 @channel="users_insert", @conninfo_hash={:dbname=>"celluloid_io_pg_listener_test"}, @super_signature=[]>>
|
60
65
|
|
61
|
-
>>
|
66
|
+
>> server.start
|
67
|
+
=> #<Celluloid::Proxy::Async(CelluloidIOPGListener::Examples::Server)>
|
62
68
|
|
63
|
-
|
69
|
+
D, [2015-10-14T12:59:36.115639 #23209] DEBUG -- : Notified users_insert
|
70
|
+
D, [2015-10-14T12:59:37.107589 #23209] DEBUG -- : Notified users_insert
|
71
|
+
D, [2015-10-14T12:59:38.112089 #23209] DEBUG -- : Notified users_insert
|
72
|
+
D, [2015-10-14T12:59:39.112341 #23209] DEBUG -- : Notified users_insert
|
73
|
+
```
|
74
|
+
|
75
|
+
The notifications will just keep flowing, 1 per second as the example server is configured by default. Now in another irb session:
|
64
76
|
|
65
|
-
I, [2015-10-06T12:40:38.110541 #5952] INFO -- : Client will for notifications on test_database:test_channel
|
66
|
-
I, [2015-10-06T12:40:38.110822 #5952] INFO -- : Starting Listening
|
67
|
-
I, [2015-10-06T12:40:50.117444 #5952] INFO -- : Received notification: ["test", 5968, "1444160450"]
|
68
|
-
I, [2015-10-06T12:40:50.117518 #5952] INFO -- : Doing Something with Payload: 1444160450 on test
|
69
|
-
I, [2015-10-06T12:40:50.117541 #5952] INFO -- : 1444160450
|
70
|
-
I, [2015-10-06T12:40:51.107977 #5952] INFO -- : Received notification: ["test", 5968, "1444160451"]
|
71
|
-
I, [2015-10-06T12:40:51.108071 #5952] INFO -- : Doing Something with Payload: 1444160451 on test
|
72
|
-
I, [2015-10-06T12:40:51.108104 #5952] INFO -- : 1444160451
|
73
|
-
I, [2015-10-06T12:40:52.112797 #5952] INFO -- : Received notification: ["test", 5968, "1444160452"]
|
74
|
-
I, [2015-10-06T12:40:52.112881 #5952] INFO -- : Doing Something with Payload: 1444160452 on test
|
75
|
-
I, [2015-10-06T12:40:52.112911 #5952] INFO -- : 1444160452
|
76
77
|
```
|
78
|
+
>> CelluloidIOPGListener::Examples::ListenerClientByInheritance.new(dbname: "celluloid_io_pg_listener_test", channel: "users_insert", callback_method: :foo_bar)
|
77
79
|
|
78
|
-
|
80
|
+
=> #<Celluloid::Proxy::Cell(CelluloidIOPGListener::Examples::ListenerClientByInheritance:0x3fe50d93f15c) @client_extracted_signature=#<CelluloidIOPGListener::Initialization::ClientExtractedSignature:0x007fca1b27c738 @channel="users_insert", @conninfo_hash={:dbname=>"celluloid_io_pg_listener_test"}, @super_signature=[{}]> @callback_method=:foo_bar @listening=true @pg_connection=#<PG::Connection:0x007fca1b287b38> @actions={"users_insert"=>:foo_bar}>
|
79
81
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
82
|
+
I, [2015-10-14T12:59:46.127120 #23223] INFO -- : Received notification: ["users_insert", 23220, "1444852786"]
|
83
|
+
I, [2015-10-14T12:59:47.127021 #23223] INFO -- : Received notification: ["users_insert", 23220, "1444852787"]
|
84
|
+
I, [2015-10-14T12:59:48.127152 #23223] INFO -- : Received notification: ["users_insert", 23220, "1444852788"]
|
85
|
+
I, [2015-10-14T12:59:49.127509 #23223] INFO -- : Received notification: ["users_insert", 23220, "1444852789"]
|
86
|
+
```
|
84
87
|
|
85
|
-
|
88
|
+
Simply exit the sessions to end the test.
|
86
89
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
90
|
+
Or keep the client running, and only exit the server and do a real test.
|
91
|
+
|
92
|
+
If you have downloaded the gem source, cd to the gem's directory, and run `bin/setup` to create the test database.
|
93
|
+
|
94
|
+
Open `psql` and `\c celluloid_io_pg_listener_test`
|
95
|
+
|
96
|
+
```
|
97
|
+
pboling=# \c celluloid_io_pg_listener_test
|
98
|
+
You are now connected to database "celluloid_io_pg_listener_test" as user "pboling".
|
99
|
+
celluloid_io_pg_listener_test=# insert into users (name) values ('jack');
|
100
|
+
NOTICE: INSERT TRIGGER called on users
|
101
|
+
INSERT 0 1
|
102
|
+
celluloid_io_pg_listener_test=# insert into users (name) values ('jill');
|
103
|
+
NOTICE: INSERT TRIGGER called on users
|
104
|
+
INSERT 0 1
|
105
|
+
celluloid_io_pg_listener_test=# \q
|
106
|
+
```
|
107
|
+
|
108
|
+
In the irb session with the Client still running you will see new notifications:
|
109
|
+
|
110
|
+
```
|
111
|
+
I, [2015-10-14T13:42:55.511294 #25295] INFO -- : Received notification: ["users_insert", 25304, "{\"table\" : \"users\", \"id\" : 1, \"name\" : \"jack\", \"type\" : \"INSERT\"}"]
|
112
|
+
I, [2015-10-14T13:43:03.841323 #25295] INFO -- : Received notification: ["users_insert", 25304, "{\"table\" : \"users\", \"id\" : 2, \"name\" : \"jill\", \"type\" : \"INSERT\"}"]
|
113
|
+
```
|
93
114
|
|
94
|
-
|
95
|
-
|
96
|
-
|
115
|
+
A more advanced client could be made to deserialize that JSON and get real work done. That's where you come in! ;)
|
116
|
+
|
117
|
+
The example [Client (Listener) class included with the gem](https://github.com/pboling/celluloid-io-pg-listener/blob/master/lib/celluloid-io-pg-listener/examples/client.rb) is just a proof of concept. It shows you how to use the `CelluloidIOPGListener::Client` module to make your own listener class that does what you need done. You could, for example, push the payload of the notification to Redis, to be worked by Sidekiq or Resque.
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
module CelluloidIOPGListener
|
121
|
+
module Examples
|
122
|
+
class Client
|
123
|
+
|
124
|
+
include CelluloidIOPGListener::Client
|
125
|
+
|
126
|
+
# Defining initialize is optional,
|
127
|
+
# unless you have custom args you need to handle
|
128
|
+
# aside from those used by the CelluloidIOPGListener::Client
|
129
|
+
# But if you do define it, use a splat,
|
130
|
+
# hash or array splat should work,
|
131
|
+
# depending on your signature needs.
|
132
|
+
# With either splat, only pass the splat params to super,
|
133
|
+
# and handle all other params locally.
|
134
|
+
#
|
135
|
+
# def initialize(optional_arg = nil, *options)
|
136
|
+
# @optional_arg = optional_arg # handle it here, don't pass it on!
|
137
|
+
# super(*options)
|
138
|
+
# end
|
139
|
+
|
140
|
+
def insert_callback(channel, payload)
|
141
|
+
# <-- within the unlisten_wrapper's block if :insert_callback is the callback_method
|
142
|
+
debug "#{self.class} channel is #{channel}"
|
143
|
+
debug "#{self.class} payload is #{payload}"
|
97
144
|
end
|
98
|
-
end
|
99
145
|
|
146
|
+
end
|
100
147
|
end
|
101
148
|
end
|
102
149
|
```
|
103
150
|
|
104
151
|
## Development
|
105
152
|
|
106
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake
|
153
|
+
After checking out the repo, run `bin/setup` to install dependencies, and setup the test environment, including creating a role and a database. Then, run `appraisal rake` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
107
154
|
|
108
155
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
109
156
|
|
157
|
+
### Running the tests
|
158
|
+
|
159
|
+
Setup has been implemented with `bin/setup`, so review the file to see what it will do before you:
|
160
|
+
|
161
|
+
bin/setup
|
162
|
+
|
163
|
+
Run the specs with rake:
|
164
|
+
|
165
|
+
appraisal rake
|
166
|
+
|
167
|
+
Or, run the specs without rake:
|
168
|
+
|
169
|
+
appraisal rspec
|
170
|
+
|
171
|
+
NOTE: If you need to recreate `db/structure.sql` from the contents of the test database:
|
172
|
+
|
173
|
+
cd spec/apps
|
174
|
+
SKIP_RAILS_ROOT_OVERRIDE=true bundle exec rake db:structure:dump
|
175
|
+
|
110
176
|
## Contributing
|
111
177
|
|
112
178
|
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/celluloid-io-pg-listener. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
|
@@ -136,7 +202,6 @@ For example:
|
|
136
202
|
spec.add_dependency 'celluloid-io-pg-listener', '~> 0.1'
|
137
203
|
```
|
138
204
|
|
139
|
-
|
140
205
|
## License
|
141
206
|
|
142
207
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
CHANGED
@@ -5,4 +5,23 @@ require "celluloid-io-pg-listener"
|
|
5
5
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
7
7
|
|
8
|
+
require "rails"
|
9
|
+
require "active_record"
|
10
|
+
|
11
|
+
# this logging style not compatible with Travis
|
12
|
+
# ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/spec/apps/log/debug.log")
|
13
|
+
ActiveRecord::Migration.verbose = false
|
14
|
+
|
15
|
+
database_yml_filepath = File.dirname(__FILE__) + "/spec/apps/config/database.yml"
|
16
|
+
configs = YAML.load_file(database_yml_filepath)
|
17
|
+
if RUBY_PLATFORM == "java"
|
18
|
+
configs["test"]["adapter"] = "jdbcpostgresql"
|
19
|
+
end
|
20
|
+
ActiveRecord::Base.configurations = configs
|
21
|
+
|
22
|
+
db_name = (ENV["DB"] || "test").to_sym
|
23
|
+
ActiveRecord::Base.establish_connection(db_name)
|
24
|
+
|
25
|
+
require "active_record/railtie"
|
26
|
+
|
8
27
|
task :default => :spec
|
data/bin/setup
CHANGED
@@ -5,3 +5,22 @@ IFS=$'\n\t'
|
|
5
5
|
bundle install
|
6
6
|
|
7
7
|
# Do any other automated setup that you need to do here
|
8
|
+
# NOTE: Assumes current user has ability to create roles in psql
|
9
|
+
puser=$(whoami)
|
10
|
+
# On Travis the user is postgres for psql
|
11
|
+
if test "$puser" = 'travis'; then
|
12
|
+
puser=postgres
|
13
|
+
fi
|
14
|
+
psql -f bin/setup.sql -U $puser
|
15
|
+
echo "Setup Role foss"
|
16
|
+
psql -U $puser << EOF
|
17
|
+
CREATE DATABASE celluloid_io_pg_listener_test;
|
18
|
+
ALTER DATABASE celluloid_io_pg_listener_test OWNER TO "foss";
|
19
|
+
EOF
|
20
|
+
echo "Setup Database celluloid_io_pg_listener_test"
|
21
|
+
|
22
|
+
cd spec/apps
|
23
|
+
bundle install
|
24
|
+
SKIP_RAILS_ROOT_OVERRIDE=true bundle exec rake test_db_setup
|
25
|
+
|
26
|
+
cd ../..
|
data/bin/setup.sql
ADDED
@@ -23,7 +23,12 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.add_dependency "pg", ">= 0.18.3"
|
24
24
|
spec.add_development_dependency "bundler", "~> 1.10"
|
25
25
|
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
-
spec.add_development_dependency "rspec"
|
27
|
-
spec.add_development_dependency "rspec-rails"
|
28
|
-
spec.add_development_dependency "appraisal"
|
26
|
+
spec.add_development_dependency "rspec", "~> 3.3"
|
27
|
+
spec.add_development_dependency "rspec-rails", "~> 3.3"
|
28
|
+
spec.add_development_dependency "appraisal", "~> 2.1"
|
29
|
+
spec.add_development_dependency "activerecord", ">= 3.2"
|
30
|
+
spec.add_development_dependency "database_cleaner", "~> 1.5"
|
31
|
+
spec.add_development_dependency "test-unit", "~> 3.1"
|
32
|
+
spec.add_development_dependency "pry", "~> 0.10"
|
33
|
+
|
29
34
|
end
|
@@ -62,6 +62,7 @@ GEM
|
|
62
62
|
timers (>= 4.1.1)
|
63
63
|
celluloid-supervision (0.20.5)
|
64
64
|
timers (>= 4.1.1)
|
65
|
+
database_cleaner (1.5.0)
|
65
66
|
diff-lcs (1.2.5)
|
66
67
|
erubis (2.7.0)
|
67
68
|
hike (1.2.3)
|
@@ -141,9 +142,11 @@ PLATFORMS
|
|
141
142
|
ruby
|
142
143
|
|
143
144
|
DEPENDENCIES
|
145
|
+
activerecord
|
144
146
|
appraisal
|
145
147
|
bundler (~> 1.10)
|
146
148
|
celluloid-io-pg-listener!
|
149
|
+
database_cleaner
|
147
150
|
pg
|
148
151
|
rails (~> 3.2.22)
|
149
152
|
rake (~> 10.0)
|
@@ -70,6 +70,7 @@ GEM
|
|
70
70
|
timers (>= 4.1.1)
|
71
71
|
celluloid-supervision (0.20.5)
|
72
72
|
timers (>= 4.1.1)
|
73
|
+
database_cleaner (1.5.0)
|
73
74
|
diff-lcs (1.2.5)
|
74
75
|
erubis (2.7.0)
|
75
76
|
globalid (0.3.6)
|
@@ -154,9 +155,11 @@ PLATFORMS
|
|
154
155
|
ruby
|
155
156
|
|
156
157
|
DEPENDENCIES
|
158
|
+
activerecord
|
157
159
|
appraisal
|
158
160
|
bundler (~> 1.10)
|
159
161
|
celluloid-io-pg-listener!
|
162
|
+
database_cleaner
|
160
163
|
pg
|
161
164
|
rails (~> 4.2.4)
|
162
165
|
rake (~> 10.0)
|
@@ -6,6 +6,11 @@ require "pg"
|
|
6
6
|
# Define the namespace this gem uses
|
7
7
|
module CelluloidIOPGListener; end
|
8
8
|
|
9
|
+
require "celluloid-io-pg-listener/initialization/client_extracted_signature"
|
10
|
+
require "celluloid-io-pg-listener/initialization/argument_extraction"
|
11
|
+
require "celluloid-io-pg-listener/initialization/async_listener"
|
9
12
|
require "celluloid-io-pg-listener/client"
|
10
|
-
require "celluloid-io-pg-listener/
|
11
|
-
require "celluloid-io-pg-listener/
|
13
|
+
require "celluloid-io-pg-listener/examples/client"
|
14
|
+
require "celluloid-io-pg-listener/examples/server"
|
15
|
+
require "celluloid-io-pg-listener/examples/listener_client_by_inheritance"
|
16
|
+
require "celluloid-io-pg-listener/examples/notify_server_by_inheritance"
|
@@ -2,19 +2,31 @@
|
|
2
2
|
module CelluloidIOPGListener
|
3
3
|
module Client
|
4
4
|
|
5
|
+
class InvalidClient < StandardError; end
|
6
|
+
|
5
7
|
def self.included(base)
|
6
8
|
base.send(:include, Celluloid)
|
7
9
|
base.send(:include, Celluloid::IO)
|
8
10
|
base.send(:include, Celluloid::Internals::Logger)
|
11
|
+
# order of prepended modules is critical if they are enhancing
|
12
|
+
# the same method(s), and they are.
|
13
|
+
base.prepend CelluloidIOPGListener::Initialization::AsyncListener
|
14
|
+
base.prepend CelluloidIOPGListener::Initialization::ArgumentExtraction
|
9
15
|
end
|
10
16
|
|
11
17
|
def unlisten_wrapper(channel, payload, &block)
|
12
|
-
|
13
|
-
|
18
|
+
if block_given?
|
19
|
+
debug "Acting on payload: #{payload} on #{channel}"
|
20
|
+
instance_eval(&block)
|
21
|
+
else
|
22
|
+
info "Not acting on payload: #{payload} on #{channel}"
|
23
|
+
end
|
14
24
|
rescue => e
|
15
|
-
info "#{self.class} disconnected from #{channel} via #{e.class} #{e.message}"
|
25
|
+
info "#{self.class}##{callback_method} disconnected from #{channel} via #{e.class} #{e.message}"
|
16
26
|
unlisten(channel)
|
17
27
|
terminate
|
28
|
+
# Rescue the error in a daemon-error-reporter to send to Airbrake or other reporting service?
|
29
|
+
raise
|
18
30
|
end
|
19
31
|
|
20
32
|
def actions
|
@@ -22,7 +34,7 @@ module CelluloidIOPGListener
|
|
22
34
|
end
|
23
35
|
|
24
36
|
def pg_connection
|
25
|
-
@pg_connection ||= PG.connect(
|
37
|
+
@pg_connection ||= PG.connect(conninfo_hash)
|
26
38
|
end
|
27
39
|
|
28
40
|
def notify(channel, value)
|
@@ -35,10 +47,7 @@ module CelluloidIOPGListener
|
|
35
47
|
end
|
36
48
|
|
37
49
|
def start_listening
|
38
|
-
info "Starting Listening"
|
39
|
-
|
40
50
|
@listening = true
|
41
|
-
|
42
51
|
wait_for_notify do |channel, pid, payload|
|
43
52
|
info "Received notification: #{[channel, pid, payload].inspect}"
|
44
53
|
send(actions[channel], channel, payload)
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module CelluloidIOPGListener
|
2
|
+
module Examples
|
3
|
+
class Client
|
4
|
+
|
5
|
+
include CelluloidIOPGListener::Client
|
6
|
+
|
7
|
+
# Defining initialize is optional,
|
8
|
+
# unless you have custom args you need to handle
|
9
|
+
# aside from those used by the CelluloidIOPGListener::Client
|
10
|
+
# But if you do define it, use a splat,
|
11
|
+
# hash or array splat should work,
|
12
|
+
# depending on your signature needs.
|
13
|
+
# With either splat, only pass the splat params to super,
|
14
|
+
# and handle all other params locally.
|
15
|
+
#
|
16
|
+
# def initialize(optional_arg = nil, *options)
|
17
|
+
# @optional_arg = optional_arg # handle it here, don't pass it on!
|
18
|
+
# super(*options)
|
19
|
+
# end
|
20
|
+
|
21
|
+
def insert_callback(channel, payload)
|
22
|
+
# <-- within the unlisten_wrapper's block if :insert_callback is the callback_method
|
23
|
+
debug "#{self.class} channel is #{channel}"
|
24
|
+
debug "#{self.class} payload is #{payload}"
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module InitializerEnhancer
|
2
|
+
|
3
|
+
def initialize(callback_method:)
|
4
|
+
# Class including this one must define the callback method
|
5
|
+
define_singleton_method(callback_method) {
|
6
|
+
super("banana") # <- Works just like you hoped it would.
|
7
|
+
}
|
8
|
+
super()
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
class SillyMonkey
|
14
|
+
|
15
|
+
prepend InitializerEnhancer
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
puts "LOL Nothing happened."
|
19
|
+
end
|
20
|
+
|
21
|
+
def eat_thing(thing)
|
22
|
+
raise RuntimeError, "I ate a #{thing}"
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
class HappyMonkey < SillyMonkey
|
28
|
+
|
29
|
+
def eat_thing(thing)
|
30
|
+
puts "Dreaming about #{thing}"
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
# >> a = HappyMonkey.new(callback_method: "eat_thing")
|
36
|
+
# LOL Nothing happened.
|
37
|
+
# => #<HappyMonkey:0x007fc77a988340>
|
38
|
+
# >> a.eat_thing
|
39
|
+
# "banana"
|
40
|
+
# => nil
|
41
|
+
# >> b = SillyMonkey.new(callback_method: "eat_thing")
|
42
|
+
# LOL Nothing happened.
|
43
|
+
# => #<SillyMonkey:0x007fc77a95a530>
|
44
|
+
# >> b.eat_thing
|
45
|
+
# RuntimeError: I ate a banana
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module CelluloidIOPGListener
|
2
|
+
module Examples
|
3
|
+
class ListenerClientByInheritance < CelluloidIOPGListener::Examples::Client
|
4
|
+
|
5
|
+
#
|
6
|
+
# When you are:
|
7
|
+
# * sub-classing a class that includes the Client and defines initialize
|
8
|
+
#
|
9
|
+
# If you
|
10
|
+
# * defining a custom / overridden initialize method in the sub-class
|
11
|
+
#
|
12
|
+
# The Client may not work.
|
13
|
+
# * the initialize overrides may happen out of normal order,
|
14
|
+
# and may not work as expected.
|
15
|
+
#
|
16
|
+
# Working Example of overridden initialize follows!
|
17
|
+
#
|
18
|
+
def initialize(a = nil, b = nil, bus: nil, fat: nil, **args)
|
19
|
+
# Unlike in the original class, the prepends have been usurped since this overridden method is now highest precedence.
|
20
|
+
super(subclassed_client: true, **args)
|
21
|
+
# The initialize overrides will be called by super, and thus you have to be careful how you pass on the arguments to super.
|
22
|
+
end
|
23
|
+
|
24
|
+
# callback_method does *not* accept a block parameter
|
25
|
+
def foo_bar(channel, payload)
|
26
|
+
# <-- within the unlisten_wrapper's block if :foo_bar is the callback_method
|
27
|
+
debug "#{self.class}##{__method__} channel: #{channel}"
|
28
|
+
debug "#{self.class}##{__method__} payload: #{payload}"
|
29
|
+
raise RuntimeError, "This example only works on the users_insert channel, you are notifying #{channel} with #{payload}" unless channel == "users_insert"
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# Simple example of a server for sending notifications through postgresql that can be listened for.
|
2
|
+
module CelluloidIOPGListener
|
3
|
+
module Examples
|
4
|
+
class Server
|
5
|
+
|
6
|
+
include Celluloid
|
7
|
+
include Celluloid::IO
|
8
|
+
include Celluloid::Internals::Logger
|
9
|
+
prepend CelluloidIOPGListener::Initialization::ArgumentExtraction
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
debug "Server will send notifications to #{dbname}:#{channel}"
|
13
|
+
end
|
14
|
+
|
15
|
+
# Defaults:
|
16
|
+
# 1/10th of a second sleep intervals
|
17
|
+
# 1 second run intervals
|
18
|
+
def start(run_interval: 1, sleep_interval: 0.1)
|
19
|
+
@sleep_interval = sleep_interval
|
20
|
+
@run_interval = run_interval
|
21
|
+
async.run
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
now = Time.now.to_f
|
26
|
+
sleep now.ceil - now + @sleep_interval
|
27
|
+
# There is no way to pass anything into the block, which is why this server isn't all that useful.
|
28
|
+
# The client is intended to listen to notifications coming from other sources,
|
29
|
+
# like a PG TRIGGER than sends a notification on INSERT, for example.
|
30
|
+
every(@run_interval) { ping }
|
31
|
+
end
|
32
|
+
|
33
|
+
# Helps with testing by making the notify synchronous.
|
34
|
+
def ping
|
35
|
+
notify(channel, Time.now.to_i)
|
36
|
+
debug "Notified #{channel}"
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def pg_connection
|
42
|
+
@pg_connection ||= PG.connect(conninfo_hash)
|
43
|
+
end
|
44
|
+
|
45
|
+
def notify(channel, value)
|
46
|
+
pg_connection.exec("NOTIFY #{channel}, '#{value}';")
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module CelluloidIOPGListener
|
2
|
+
# Prepended to classes including the CelluloidIOPGListener::Client
|
3
|
+
# Takes the arguments relevant to the CelluloidIOPGListener::Client module
|
4
|
+
# and extracts it leaving the rest of the arguments to be passed to the
|
5
|
+
# initializer of classes including the CelluloidIOPGListener::Client
|
6
|
+
module Initialization
|
7
|
+
module ArgumentExtraction
|
8
|
+
|
9
|
+
# 1st initialize override invoked
|
10
|
+
def initialize(*args)
|
11
|
+
@client_extracted_signature = CelluloidIOPGListener::Initialization::ClientExtractedSignature.new(*args)
|
12
|
+
# When called from a sub-class of a class including Client
|
13
|
+
# and the sub-class overrides initialize,
|
14
|
+
# then the execution order changes,
|
15
|
+
# and this method may no longer have a super.
|
16
|
+
# However, due to the nature of the initialize method we can't tell if we have a legitimate super or not.
|
17
|
+
super(*@client_extracted_signature.super_signature)
|
18
|
+
end
|
19
|
+
|
20
|
+
def dbname
|
21
|
+
conninfo_hash[:dbname]
|
22
|
+
end
|
23
|
+
|
24
|
+
def channel
|
25
|
+
@client_extracted_signature.channel
|
26
|
+
end
|
27
|
+
|
28
|
+
def conninfo_hash
|
29
|
+
@client_extracted_signature.conninfo_hash
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module CelluloidIOPGListener
|
2
|
+
# Prepended to classes including the CelluloidIOPGListener::Client
|
3
|
+
module Initialization
|
4
|
+
module AsyncListener
|
5
|
+
|
6
|
+
# 2nd initialize override invoked
|
7
|
+
# @callback_method - Name of the method to be called when notifications are heard
|
8
|
+
# Default: :unlisten_wrapper (just to have a guaranteed method with the correct arity)
|
9
|
+
def initialize(*args)
|
10
|
+
hash_arg = args.last.is_a?(Hash) ? args.pop : {}
|
11
|
+
warn "[#{self.class}] You have not specified a callback_method, so :unlisten_wrapper will be used." unless hash_arg[:callback_method]
|
12
|
+
@callback_method = hash_arg.delete(:callback_method) || :unlisten_wrapper
|
13
|
+
# Doesn't appear to be any other way to make it work with subclassing,
|
14
|
+
# due to the way Celluloid Proxies the class, and hijacks the inheritance chains
|
15
|
+
subclassed_client = hash_arg.delete(:subclassed_client) || false
|
16
|
+
args << hash_arg unless hash_arg.empty?
|
17
|
+
|
18
|
+
enhance_callback_method unless @callback_method == :unlisten_wrapper
|
19
|
+
|
20
|
+
# When called from a sub-class of a class including Client
|
21
|
+
# and the sub-class overrides initialize,
|
22
|
+
# then the execution order changes,
|
23
|
+
# and this method may no longer have a super.
|
24
|
+
# However, due to the nature of the initialize method we can't tell if we have a legitimate super or not.
|
25
|
+
super(*args) if !subclassed_client
|
26
|
+
|
27
|
+
debug "Listening for notifications on #{dbname}:#{channel} with callback to #{@callback_method}"
|
28
|
+
async.start_listening
|
29
|
+
async.listen(channel, @callback_method)
|
30
|
+
end
|
31
|
+
|
32
|
+
def callback_method
|
33
|
+
@callback_method
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def enhance_callback_method
|
39
|
+
# The class including CelluloidIOPGListener::Client must define
|
40
|
+
# the method named by @callback_method
|
41
|
+
define_singleton_method(@callback_method) do |channel, payload|
|
42
|
+
unlisten_wrapper(channel, payload) do
|
43
|
+
if defined?(super)
|
44
|
+
super(channel, payload)
|
45
|
+
else
|
46
|
+
error "LISTENER ERROR: #{payload}"
|
47
|
+
raise CelluloidIOPGListener::Client::InvalidClient, "#{self.class} does not define a method :#{@callback_method} with arguments (channel, payload)"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module CelluloidIOPGListener
|
2
|
+
module Initialization
|
3
|
+
#
|
4
|
+
# Null Object Pattern, just in case there are no options passed to initialize
|
5
|
+
#
|
6
|
+
# @conninfo_hash - Extracted (actually removed!) PG database connection options, such as would be sent to:
|
7
|
+
# PG.connect( *args ) # PG::Connection.new( *args )
|
8
|
+
# Options must be the same named parameters that PG.connect() expects in its argument hash
|
9
|
+
# The other parameter formats are accepted by PG::Connection.new are not supported here.
|
10
|
+
# Named after, and structurally identical to, PG::Connection#conninfo_hash
|
11
|
+
# @super_signature - Arguments passed on to super, supports any type of argument signature / arity supported by Ruby itself.
|
12
|
+
# @channel - The channel to listen to notifications on
|
13
|
+
# Default: None, raises an error if not provided.
|
14
|
+
#
|
15
|
+
class ClientExtractedSignature
|
16
|
+
|
17
|
+
# see http://deveiate.org/code/pg/PG/Connection.html for key meanings
|
18
|
+
KEYS = [:host, :hostaddr, :port, :dbname, :user, :password, :connect_timeout, :options, :tty, :sslmode, :krbsrvname, :gsslib, :service]
|
19
|
+
|
20
|
+
attr_reader :super_signature
|
21
|
+
attr_reader :conninfo_hash
|
22
|
+
attr_reader :channel
|
23
|
+
|
24
|
+
# args - an array
|
25
|
+
def initialize(*args)
|
26
|
+
hash_arg = args.last.is_a?(Hash) ? args.pop : {}
|
27
|
+
# Extract the channel first, as it is required
|
28
|
+
@channel = hash_arg.delete(:channel) || raise(ArgumentError, "[#{self.class}] :channel is required, but got #{args} and #{hash_arg}")
|
29
|
+
# Extract the args for PG.connect
|
30
|
+
@conninfo_hash = (hash_arg.keys & KEYS).
|
31
|
+
each_with_object({}) { |k,h| h.update(k => hash_arg.delete(k)) }.
|
32
|
+
# Future proof. Provide a way to send in any PG.connect() options not explicitly defined in KEYS
|
33
|
+
merge(hash_arg.delete(:conninfo_hash) || {})
|
34
|
+
# Add any other named parameters back to the args for super
|
35
|
+
args << hash_arg unless hash_arg.empty?
|
36
|
+
@super_signature = args
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: celluloid-io-pg-listener
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Boling
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-10-
|
11
|
+
date: 2015-10-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: celluloid-io
|
@@ -70,44 +70,100 @@ dependencies:
|
|
70
70
|
name: rspec
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
|
-
- - "
|
73
|
+
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: '
|
75
|
+
version: '3.3'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
|
-
- - "
|
80
|
+
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
82
|
+
version: '3.3'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
84
|
name: rspec-rails
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- - "
|
87
|
+
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '
|
89
|
+
version: '3.3'
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- - "
|
94
|
+
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version: '
|
96
|
+
version: '3.3'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
98
|
name: appraisal
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '2.1'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '2.1'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: activerecord
|
99
113
|
requirement: !ruby/object:Gem::Requirement
|
100
114
|
requirements:
|
101
115
|
- - ">="
|
102
116
|
- !ruby/object:Gem::Version
|
103
|
-
version: '
|
117
|
+
version: '3.2'
|
104
118
|
type: :development
|
105
119
|
prerelease: false
|
106
120
|
version_requirements: !ruby/object:Gem::Requirement
|
107
121
|
requirements:
|
108
122
|
- - ">="
|
109
123
|
- !ruby/object:Gem::Version
|
110
|
-
version: '
|
124
|
+
version: '3.2'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: database_cleaner
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '1.5'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '1.5'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: test-unit
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '3.1'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '3.1'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: pry
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0.10'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0.10'
|
111
167
|
description: Asynchronously LISTEN for Postgresql NOTIFY messages with payloads and
|
112
168
|
Do Something
|
113
169
|
email:
|
@@ -127,6 +183,7 @@ files:
|
|
127
183
|
- Rakefile
|
128
184
|
- bin/console
|
129
185
|
- bin/setup
|
186
|
+
- bin/setup.sql
|
130
187
|
- celluloid-io-pg-listener.gemspec
|
131
188
|
- gemfiles/rails_3.2.22.gemfile
|
132
189
|
- gemfiles/rails_3.2.22.gemfile.lock
|
@@ -134,8 +191,14 @@ files:
|
|
134
191
|
- gemfiles/rails_4.2.4.gemfile.lock
|
135
192
|
- lib/celluloid-io-pg-listener.rb
|
136
193
|
- lib/celluloid-io-pg-listener/client.rb
|
137
|
-
- lib/celluloid-io-pg-listener/
|
138
|
-
- lib/celluloid-io-pg-listener/
|
194
|
+
- lib/celluloid-io-pg-listener/examples/client.rb
|
195
|
+
- lib/celluloid-io-pg-listener/examples/double_super_example.rb
|
196
|
+
- lib/celluloid-io-pg-listener/examples/listener_client_by_inheritance.rb
|
197
|
+
- lib/celluloid-io-pg-listener/examples/notify_server_by_inheritance.rb
|
198
|
+
- lib/celluloid-io-pg-listener/examples/server.rb
|
199
|
+
- lib/celluloid-io-pg-listener/initialization/argument_extraction.rb
|
200
|
+
- lib/celluloid-io-pg-listener/initialization/async_listener.rb
|
201
|
+
- lib/celluloid-io-pg-listener/initialization/client_extracted_signature.rb
|
139
202
|
- lib/celluloid-io-pg-listener/version.rb
|
140
203
|
homepage: https://github.com/pboling/celluloid-io-pg-listener
|
141
204
|
licenses:
|
@@ -1,21 +0,0 @@
|
|
1
|
-
module CelluloidIOPGListener
|
2
|
-
# An example Client class
|
3
|
-
class Listener
|
4
|
-
|
5
|
-
include CelluloidIOPGListener::Client
|
6
|
-
|
7
|
-
def initialize(dbname:, channel:)
|
8
|
-
info "Client will for notifications on #{dbname}:#{channel}"
|
9
|
-
@dbname = dbname
|
10
|
-
async.start_listening
|
11
|
-
async.listen(channel, :do_something)
|
12
|
-
end
|
13
|
-
|
14
|
-
def do_something(channel, payload)
|
15
|
-
unlisten_wrapper(channel, payload) do
|
16
|
-
info payload
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
end
|
21
|
-
end
|
@@ -1,38 +0,0 @@
|
|
1
|
-
# Simple example of a server for sending notifications through postgresql that can be listened for.
|
2
|
-
class CelluloidIOPGListener::Server
|
3
|
-
|
4
|
-
include Celluloid
|
5
|
-
include Celluloid::IO
|
6
|
-
include Celluloid::Internals::Logger
|
7
|
-
|
8
|
-
# Defaults:
|
9
|
-
# 1/10th of a second sleep intervals
|
10
|
-
# 1 second run intervals
|
11
|
-
def initialize(dbname:, channel:, run_interval: 1, sleep_interval: 0.1)
|
12
|
-
info "Server will send notifications to #{dbname}:#{channel}"
|
13
|
-
@dbname = dbname
|
14
|
-
@channel = channel
|
15
|
-
@sleep_interval = sleep_interval
|
16
|
-
@run_interval = run_interval
|
17
|
-
async.run
|
18
|
-
end
|
19
|
-
|
20
|
-
def run
|
21
|
-
now = Time.now.to_f
|
22
|
-
sleep now.ceil - now + @sleep_interval
|
23
|
-
# There is no way to pass anything into the block, which is why this server isn't all that useful.
|
24
|
-
# The client is intended to listen to notifications coming from other sources,
|
25
|
-
# like a PG TRIGGER than sends a notification on INSERT, for example.
|
26
|
-
every(@run_interval) { notify(@channel, Time.now.to_i) }
|
27
|
-
info "Notified #{@channel}"
|
28
|
-
end
|
29
|
-
|
30
|
-
def pg_connection
|
31
|
-
@pg_connection ||= PG.connect( dbname: @dbname )
|
32
|
-
end
|
33
|
-
|
34
|
-
def notify(channel, value)
|
35
|
-
pg_connection.exec("NOTIFY #{channel}, '#{value}';")
|
36
|
-
end
|
37
|
-
|
38
|
-
end
|