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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6688ff5db3f6b68ae8cb6c3d30d1c7324255285d
4
- data.tar.gz: 1aa1956925611e9bbdda6a966dca67d872a0ebf9
3
+ metadata.gz: 1f0e16f28d6bd7ddda7c1cce2480269924c11690
4
+ data.tar.gz: 6d89f93dcdc3dea378d729e4d9adccb06e8427d0
5
5
  SHA512:
6
- metadata.gz: 7d6313905452f4c1ba7521ea991688d53bca029de7bbec76df0ce16e791cc66d5e4bb8285a44f27e9855a2d1f84ffde0815f4cc9088ab349fd4a00b1848ac762
7
- data.tar.gz: 67c96d9e54a750629e46f7cd996ded67a034e056ee537b72337acc1d0f319a05ce11d59db7392ad86397c9c893f39c58132f3f5f84681d5c5dcd9dbde1af015c
6
+ metadata.gz: c7e78c2385b61f25dcd7111460cd463f76b89feece98a31e27f5b7f82b3e5be097e0326dced44f2571f3da022203f072390d32e9cb3f3cd6a317cdd994ae76c7
7
+ data.tar.gz: 42ec7cf6cce2f8dbe724d1cb22b7f2e146f0eeca070479b1c602afe2d3031fe6f1d2c283f4a2b6fbd8cb013a2d68a9dcb0ddc53a61d707213212e5ef6e504eb5
data/.rspec CHANGED
@@ -1,2 +1,2 @@
1
- --format documentation
2
1
  --color
2
+ --require spec_helper
data/.travis.yml CHANGED
@@ -1,8 +1,12 @@
1
1
  language: ruby
2
+ cache: bundler
2
3
  rvm:
3
- - 2.1.2
4
+ - 2.1.5
4
5
  - 2.2.3
5
- before_install: gem install bundler -v 1.10.6
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
@@ -2,3 +2,4 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in celluloid-io-pg-listener.gemspec
4
4
  gemspec
5
+
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
- >> CelluloidIOPGListener::Server.new(dbname: "test_database", channel: "test_channel" )
57
+ >> $CELLULOID_DEBUG=true
58
+ => true
54
59
 
55
- I, [2015-10-06T12:38:43.728686 #5880] INFO -- : Server will send notifications to archer_test:test
60
+ >> server = CelluloidIOPGListener::Examples::Server.new(dbname: "celluloid_io_pg_listener_test", channel: "users_insert")
56
61
 
57
- => #<Celluloid::Proxy::Cell(CelluloidIOPGListener::Server:0x3ff732194f24) @dbname="test_database" @channel="test_channel" @sleep_interval=0.1 @run_interval=1>
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
- I, [2015-10-06T12:38:44.105265 #5880] INFO -- : Notified test
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
- >> CelluloidIOPGListener::Listener.new(dbname: "test_database", channel: "test_channel" )
66
+ >> server.start
67
+ => #<Celluloid::Proxy::Async(CelluloidIOPGListener::Examples::Server)>
62
68
 
63
- => #<Celluloid::Proxy::Cell(CelluloidIOPGListener::Listener:0x3fd6ace33cb8) @dbname="test_database" @listening=true @pg_connection=#<PG::Connection:0x007fad59c5f978> @actions={"test_channel"=>:do_something}>
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
- The Listener class included is just a proof of concept. It shows you how to use the Client module to make your own listener class that does what you need done.
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
- ```ruby
81
- module CelluloidIOPGListener
82
- # An example Client class
83
- class Listener
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
- include CelluloidIOPGListener::Client
88
+ Simply exit the sessions to end the test.
86
89
 
87
- def initialize(dbname:, channel:)
88
- info "Client will for notifications on #{dbname}:#{channel}"
89
- @dbname = dbname
90
- async.start_listening
91
- async.listen(channel, :do_something)
92
- end
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
- def do_something(channel, payload)
95
- unlisten_wrapper(channel, payload) do
96
- info payload
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 spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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
@@ -0,0 +1,12 @@
1
+ DO
2
+ $body$
3
+ BEGIN
4
+ IF NOT EXISTS (
5
+ SELECT *
6
+ FROM pg_catalog.pg_user
7
+ WHERE usename = 'foss') THEN
8
+
9
+ CREATE ROLE foss WITH CREATEDB LOGIN;
10
+ END IF;
11
+ END
12
+ $body$
@@ -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/server"
11
- require "celluloid-io-pg-listener/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
- info "Doing Something with Payload: #{payload} on #{channel}"
13
- instance_eval(&block)
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( dbname: @dbname )
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,7 @@
1
+ module CelluloidIOPGListener
2
+ module Examples
3
+ class NotifyServerByInheritance < CelluloidIOPGListener::Examples::Client
4
+
5
+ end
6
+ end
7
+ 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
@@ -1,3 +1,3 @@
1
1
  module CelluloidIOPGListener
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  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.0
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-06 00:00:00.000000000 Z
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: '0'
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: '0'
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: '0'
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: '0'
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: '0'
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: '0'
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/listener.rb
138
- - lib/celluloid-io-pg-listener/server.rb
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