couchrest_changes 0.0.5 → 0.1.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/lib/couchrest/changes.rb +5 -164
- data/lib/couchrest/changes/config.rb +2 -1
- data/lib/couchrest/changes/observer.rb +178 -0
- data/lib/couchrest/changes/version.rb +2 -2
- data/test/config.yaml +19 -0
- data/test/integration/callback_test.rb +34 -0
- data/test/setup_couch.sh +13 -0
- data/test/support/integration_test.rb +39 -0
- data/test/test_helper.rb +3 -0
- metadata +12 -3
data/lib/couchrest/changes.rb
CHANGED
@@ -3,174 +3,15 @@ require 'fileutils'
|
|
3
3
|
require 'pathname'
|
4
4
|
|
5
5
|
require 'couchrest/changes/config'
|
6
|
+
require 'couchrest/changes/observer'
|
6
7
|
|
7
8
|
module CouchRest
|
8
|
-
|
9
|
+
module Changes
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
db_name = Config.complete_db_name(db_name)
|
14
|
-
info "Tracking #{db_name}"
|
15
|
-
debug "Options: #{options.inspect}" if options.keys.any?
|
16
|
-
@options = options
|
17
|
-
@db = CouchRest.new(Config.couch_host).database(db_name)
|
18
|
-
read_seq(Config.seq_file) unless rerun?
|
19
|
-
check_seq
|
20
|
-
end
|
21
|
-
|
22
|
-
# triggered when a document was newly created
|
23
|
-
def created(hash = {}, &block)
|
24
|
-
run_or_define_hook :created, hash, &block
|
25
|
-
end
|
26
|
-
|
27
|
-
# triggered when a document was deleted
|
28
|
-
def deleted(hash = {}, &block)
|
29
|
-
run_or_define_hook :deleted, hash, &block
|
30
|
-
end
|
31
|
-
|
32
|
-
# triggered when an existing document was updated
|
33
|
-
def updated(hash = {}, &block)
|
34
|
-
run_or_define_hook :updated, hash, &block
|
35
|
-
end
|
36
|
-
|
37
|
-
# triggered whenever a document was changed
|
38
|
-
def changed(hash = {}, &block)
|
39
|
-
run_or_define_hook :changed, hash, &block
|
40
|
-
end
|
41
|
-
|
42
|
-
def listen
|
43
|
-
info "listening..."
|
44
|
-
debug "Starting at sequence #{since}"
|
45
|
-
result = db.changes feed_options do |hash|
|
46
|
-
@retry_count = 0
|
47
|
-
callbacks(hash)
|
48
|
-
store_seq(hash["seq"])
|
49
|
-
end
|
50
|
-
raise EOFError
|
51
|
-
# appearently MultiJson has issues with the end of the couch stream.
|
52
|
-
# So sometimes we get a MultiJson::LoadError instead...
|
53
|
-
rescue MultiJson::LoadError, EOFError, RestClient::ServerBrokeConnection
|
54
|
-
return if run_once?
|
55
|
-
log_and_recover(result)
|
56
|
-
retry
|
57
|
-
end
|
58
|
-
|
59
|
-
protected
|
60
|
-
|
61
|
-
def feed_options
|
62
|
-
if run_once?
|
63
|
-
{ :since => since }
|
64
|
-
else
|
65
|
-
{ :feed => :continuous, :since => since, :heartbeat => 1000 }
|
66
|
-
end.merge @options
|
67
|
-
end
|
68
|
-
|
69
|
-
def since
|
70
|
-
@since ||= 0 # fetch_last_seq
|
71
|
-
end
|
72
|
-
|
73
|
-
def callbacks(hash)
|
74
|
-
# let's not track design document changes
|
75
|
-
return if hash['id'].start_with? '_design/'
|
76
|
-
return unless changes = hash["changes"]
|
77
|
-
changed(hash)
|
78
|
-
return deleted(hash) if hash["deleted"]
|
79
|
-
return created(hash) if changes[0]["rev"].start_with?('1-')
|
80
|
-
updated(hash)
|
81
|
-
end
|
82
|
-
|
83
|
-
def run_or_define_hook(event, hash = {}, &block)
|
84
|
-
@callbacks ||= {}
|
85
|
-
if block_given?
|
86
|
-
@callbacks[event] = block
|
87
|
-
else
|
88
|
-
@callbacks[event] && @callbacks[event].call(hash)
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
def read_seq(filename)
|
93
|
-
debug "Looking up sequence here: #{filename}"
|
94
|
-
FileUtils.touch(filename)
|
95
|
-
unless File.writable?(filename)
|
96
|
-
raise StandardError.new("Can't write to sequence file #{filename}")
|
97
|
-
end
|
98
|
-
@since = File.read(filename)
|
99
|
-
rescue Errno::ENOENT => e
|
100
|
-
warn "No sequence file found. Starting from scratch"
|
101
|
-
end
|
102
|
-
|
103
|
-
def check_seq
|
104
|
-
if @since == ''
|
105
|
-
@since = nil
|
106
|
-
debug "Found no sequence in the file."
|
107
|
-
elsif @since
|
108
|
-
debug "Found sequence: #{@since}"
|
11
|
+
class << self
|
12
|
+
def new(*opts)
|
13
|
+
Observer.new(*opts)
|
109
14
|
end
|
110
15
|
end
|
111
|
-
|
112
|
-
def store_seq(seq)
|
113
|
-
File.write Config.seq_file, MultiJson.dump(seq)
|
114
|
-
end
|
115
|
-
|
116
|
-
def log_and_recover(result)
|
117
|
-
debug result.inspect if result
|
118
|
-
info "Couch stream ended unexpectedly."
|
119
|
-
info "Will retry in 15 seconds."
|
120
|
-
info "Retried #{retry_count} times so far."
|
121
|
-
sleep 15
|
122
|
-
@retry_count += 1
|
123
|
-
end
|
124
|
-
|
125
|
-
#
|
126
|
-
# UNUSED: this is useful for only following new sequences.
|
127
|
-
# might also require .to_json to work on bigcouch.
|
128
|
-
#
|
129
|
-
def fetch_last_seq
|
130
|
-
hash = db.changes :limit => 1, :descending => true
|
131
|
-
return hash["last_seq"]
|
132
|
-
end
|
133
|
-
|
134
|
-
def rerun?
|
135
|
-
Config.flags.include?('--rerun')
|
136
|
-
end
|
137
|
-
|
138
|
-
def run_once?
|
139
|
-
Config.flags.include?('--run-once')
|
140
|
-
end
|
141
|
-
|
142
|
-
def info(message)
|
143
|
-
return unless log_attempt?
|
144
|
-
logger.info message
|
145
|
-
end
|
146
|
-
|
147
|
-
def debug(message)
|
148
|
-
return unless log_attempt?
|
149
|
-
logger.debug message
|
150
|
-
end
|
151
|
-
|
152
|
-
def warn(message)
|
153
|
-
return unless log_attempt?
|
154
|
-
logger.warn message
|
155
|
-
end
|
156
|
-
|
157
|
-
# let's not clutter the logs if couch is down for a longer time.
|
158
|
-
def log_attempt?
|
159
|
-
[0, 1, 2, 4, 8, 20, 40, 120].include?(retry_count) ||
|
160
|
-
retry_count % 240 == 0
|
161
|
-
end
|
162
|
-
|
163
|
-
def retry_count
|
164
|
-
@retry_count ||= 0
|
165
|
-
end
|
166
|
-
|
167
|
-
def logger
|
168
|
-
logger ||= Config.logger
|
169
|
-
end
|
170
|
-
|
171
|
-
def db
|
172
|
-
@db
|
173
|
-
end
|
174
|
-
|
175
16
|
end
|
176
17
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'yaml'
|
2
2
|
|
3
3
|
module CouchRest
|
4
|
-
|
4
|
+
module Changes
|
5
5
|
module Config
|
6
6
|
extend self
|
7
7
|
|
@@ -20,6 +20,7 @@ module CouchRest
|
|
20
20
|
file = find_file(file_path)
|
21
21
|
load_config(file)
|
22
22
|
end
|
23
|
+
self.flags ||= []
|
23
24
|
init_logger
|
24
25
|
log_loaded_configs(loaded.compact)
|
25
26
|
logger.info "Observing #{couch_host_without_password}"
|
@@ -0,0 +1,178 @@
|
|
1
|
+
module CouchRest::Changes
|
2
|
+
class Observer
|
3
|
+
|
4
|
+
attr_writer :logger
|
5
|
+
|
6
|
+
def initialize(db_name, options = {})
|
7
|
+
db_name = Config.complete_db_name(db_name)
|
8
|
+
info "Tracking #{db_name}"
|
9
|
+
debug "Options: #{options.inspect}" if options.keys.any?
|
10
|
+
@options = options
|
11
|
+
unless @db = CouchRest.new(Config.couch_host).database(db_name)
|
12
|
+
logger.error "Database #{db_name} not found!"
|
13
|
+
raise RuntimeError "Database #{db_name} not found!"
|
14
|
+
end
|
15
|
+
read_seq(Config.seq_file) unless rerun?
|
16
|
+
check_seq
|
17
|
+
end
|
18
|
+
|
19
|
+
# triggered when a document was newly created
|
20
|
+
def created(hash = {}, &block)
|
21
|
+
run_or_define_hook :created, hash, &block
|
22
|
+
end
|
23
|
+
|
24
|
+
# triggered when a document was deleted
|
25
|
+
def deleted(hash = {}, &block)
|
26
|
+
run_or_define_hook :deleted, hash, &block
|
27
|
+
end
|
28
|
+
|
29
|
+
# triggered when an existing document was updated
|
30
|
+
def updated(hash = {}, &block)
|
31
|
+
run_or_define_hook :updated, hash, &block
|
32
|
+
end
|
33
|
+
|
34
|
+
# triggered whenever a document was changed
|
35
|
+
def changed(hash = {}, &block)
|
36
|
+
run_or_define_hook :changed, hash, &block
|
37
|
+
end
|
38
|
+
|
39
|
+
def listen
|
40
|
+
info "listening..."
|
41
|
+
debug "Starting at sequence #{since}"
|
42
|
+
result = db.changes feed_options do |hash|
|
43
|
+
@retry_count = 0
|
44
|
+
callbacks(hash)
|
45
|
+
store_seq(hash["seq"])
|
46
|
+
end
|
47
|
+
raise EOFError
|
48
|
+
# appearently MultiJson has issues with the end of the couch stream.
|
49
|
+
# So sometimes we get a MultiJson::LoadError instead...
|
50
|
+
rescue MultiJson::LoadError, EOFError, RestClient::ServerBrokeConnection
|
51
|
+
retry if retry_without_sequence?(result) || retry_later?
|
52
|
+
info "Couch stream ended."
|
53
|
+
end
|
54
|
+
|
55
|
+
def last_sequence
|
56
|
+
hash = db.changes :limit => 1, :descending => true
|
57
|
+
return hash["last_seq"]
|
58
|
+
end
|
59
|
+
|
60
|
+
protected
|
61
|
+
|
62
|
+
def feed_options
|
63
|
+
if run_once?
|
64
|
+
{ :since => since }
|
65
|
+
else
|
66
|
+
{ :feed => :continuous, :since => since, :heartbeat => 1000 }
|
67
|
+
end.merge @options
|
68
|
+
end
|
69
|
+
|
70
|
+
def since
|
71
|
+
@since ||= 0 # last_sequence
|
72
|
+
end
|
73
|
+
|
74
|
+
def callbacks(hash)
|
75
|
+
# let's not track design document changes
|
76
|
+
return if hash['id'].start_with? '_design/'
|
77
|
+
return unless changes = hash["changes"]
|
78
|
+
changed(hash)
|
79
|
+
return deleted(hash) if hash["deleted"]
|
80
|
+
return created(hash) if changes[0]["rev"].start_with?('1-')
|
81
|
+
updated(hash)
|
82
|
+
end
|
83
|
+
|
84
|
+
def run_or_define_hook(event, hash = {}, &block)
|
85
|
+
@callbacks ||= {}
|
86
|
+
if block_given?
|
87
|
+
@callbacks[event] = block
|
88
|
+
else
|
89
|
+
@callbacks[event] && @callbacks[event].call(hash)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def read_seq(filename)
|
94
|
+
debug "Looking up sequence here: #{filename}"
|
95
|
+
FileUtils.touch(filename)
|
96
|
+
unless File.writable?(filename)
|
97
|
+
raise StandardError.new("Can't write to sequence file #{filename}")
|
98
|
+
end
|
99
|
+
@since = File.read(filename)
|
100
|
+
rescue Errno::ENOENT => e
|
101
|
+
warn "No sequence file found. Starting from scratch"
|
102
|
+
end
|
103
|
+
|
104
|
+
def check_seq
|
105
|
+
if @since == ''
|
106
|
+
@since = nil
|
107
|
+
debug "Found no sequence in the file."
|
108
|
+
elsif @since
|
109
|
+
debug "Found sequence: #{@since}"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def store_seq(seq)
|
114
|
+
File.write Config.seq_file, MultiJson.dump(seq)
|
115
|
+
end
|
116
|
+
|
117
|
+
def retry_without_sequence?(result)
|
118
|
+
return unless malformated_sequence?(result)
|
119
|
+
@since = nil
|
120
|
+
info "Trying to start from scratch."
|
121
|
+
end
|
122
|
+
|
123
|
+
def malformated_sequence?(result)
|
124
|
+
reason = result && result.respond_to?(:keys) && result["reason"]
|
125
|
+
reason && ( reason.include?('since') || reason == 'badarg' )
|
126
|
+
end
|
127
|
+
|
128
|
+
def retry_later?
|
129
|
+
return unless rerun?
|
130
|
+
info "Will retry in 15 seconds."
|
131
|
+
info "Retried #{retry_count} times so far."
|
132
|
+
sleep 15
|
133
|
+
@retry_count += 1
|
134
|
+
end
|
135
|
+
|
136
|
+
def rerun?
|
137
|
+
Config.flags.include?('--rerun')
|
138
|
+
end
|
139
|
+
|
140
|
+
def run_once?
|
141
|
+
Config.flags.include?('--run-once')
|
142
|
+
end
|
143
|
+
|
144
|
+
def info(message)
|
145
|
+
return unless log_attempt?
|
146
|
+
logger.info message
|
147
|
+
end
|
148
|
+
|
149
|
+
def debug(message)
|
150
|
+
return unless log_attempt?
|
151
|
+
logger.debug message
|
152
|
+
end
|
153
|
+
|
154
|
+
def warn(message)
|
155
|
+
return unless log_attempt?
|
156
|
+
logger.warn message
|
157
|
+
end
|
158
|
+
|
159
|
+
# let's not clutter the logs if couch is down for a longer time.
|
160
|
+
def log_attempt?
|
161
|
+
[0, 1, 2, 4, 8, 20, 40, 120].include?(retry_count) ||
|
162
|
+
retry_count % 240 == 0
|
163
|
+
end
|
164
|
+
|
165
|
+
def retry_count
|
166
|
+
@retry_count ||= 0
|
167
|
+
end
|
168
|
+
|
169
|
+
def logger
|
170
|
+
logger ||= Config.logger
|
171
|
+
end
|
172
|
+
|
173
|
+
def db
|
174
|
+
@db
|
175
|
+
end
|
176
|
+
|
177
|
+
end
|
178
|
+
end
|
data/test/config.yaml
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#
|
2
|
+
# Default configuration options for Tapicero
|
3
|
+
#
|
4
|
+
|
5
|
+
# couch connection configuration
|
6
|
+
connection:
|
7
|
+
protocol: "http"
|
8
|
+
host: "localhost"
|
9
|
+
port: 5984
|
10
|
+
username: anna
|
11
|
+
password: secret
|
12
|
+
prefix: "couchrest_changes_test"
|
13
|
+
suffix: ""
|
14
|
+
|
15
|
+
# file to store the last processed user record in so we can resume after
|
16
|
+
# a restart:
|
17
|
+
seq_file: "/tmp/couchrest_changes_test.seq"
|
18
|
+
log_file: "/tmp/couchrest_changes_test.log"
|
19
|
+
log_level: debug
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class CallbackTest < CouchRest::Changes::IntegrationTest
|
4
|
+
|
5
|
+
def setup
|
6
|
+
super
|
7
|
+
@config.flags = ['--run-once']
|
8
|
+
@changes = CouchRest::Changes.new 'records'
|
9
|
+
File.write config.seq_file, @changes.last_sequence
|
10
|
+
end
|
11
|
+
|
12
|
+
def teardown
|
13
|
+
delete
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_triggers_created
|
17
|
+
handler = mock 'handler'
|
18
|
+
handler.expects(:callback).once
|
19
|
+
@changes.created { |hash| handler.callback(hash) }
|
20
|
+
create
|
21
|
+
@changes.listen
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_starts_from_scratch_on_invalid_sequence
|
25
|
+
File.write config.seq_file, "invalid string"
|
26
|
+
@changes = CouchRest::Changes.new 'records'
|
27
|
+
handler = mock 'handler'
|
28
|
+
handler.expects(:callback).at_least_once
|
29
|
+
@changes.created { |hash| handler.callback(hash) }
|
30
|
+
create
|
31
|
+
@changes.listen
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
data/test/setup_couch.sh
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
HOST="http://localhost:5984"
|
4
|
+
echo "couch version :"
|
5
|
+
curl -X GET $HOST
|
6
|
+
echo "creating unprivileged user :"
|
7
|
+
curl -HContent-Type:application/json -XPUT $HOST/_users/org.couchdb.user:me --data-binary '{"_id": "org.couchdb.user:me","name": "me","roles": [],"type": "user","password": "pwd"}'
|
8
|
+
echo "creating database to watch:"
|
9
|
+
curl -X PUT $HOST/couchrest_changes_test_records
|
10
|
+
echo "restricting database access :"
|
11
|
+
curl -X PUT $HOST/couchrest_changes_test_records/_security -Hcontent-type:application/json --data-binary '{"admins":{"names":[],"roles":[]},"members":{"names":["me"],"roles":[]}}'
|
12
|
+
echo "adding admin :"
|
13
|
+
curl -X PUT $HOST/_config/admins/anna -d '"secret"'
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module CouchRest::Changes
|
2
|
+
class IntegrationTest < MiniTest::Unit::TestCase
|
3
|
+
|
4
|
+
attr_reader :config
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@config ||= CouchRest::Changes::Config.load BASE_DIR,
|
8
|
+
'test/config.yaml'
|
9
|
+
end
|
10
|
+
|
11
|
+
def create(fast = false)
|
12
|
+
result = database.save_doc :some => :content
|
13
|
+
raise RuntimeError.new(result.inspect) unless result['ok']
|
14
|
+
@record = {'_id' => result["id"], '_rev' => result["rev"]}
|
15
|
+
sleep 1
|
16
|
+
end
|
17
|
+
|
18
|
+
def delete(fast = false)
|
19
|
+
return if @record.nil? or @record['_deleted']
|
20
|
+
result = database.delete_doc @record
|
21
|
+
raise RuntimeError.new(result.inspect) unless result['ok']
|
22
|
+
@record['_deleted'] = true
|
23
|
+
sleep 1
|
24
|
+
end
|
25
|
+
|
26
|
+
def database
|
27
|
+
@database ||= host.database(database_name)
|
28
|
+
end
|
29
|
+
|
30
|
+
def database_name
|
31
|
+
config.complete_db_name('records')
|
32
|
+
end
|
33
|
+
|
34
|
+
def host
|
35
|
+
@host ||= CouchRest.new(config.couch_host)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: couchrest_changes
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-
|
12
|
+
date: 2014-07-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: couchrest
|
@@ -133,9 +133,14 @@ extra_rdoc_files: []
|
|
133
133
|
files:
|
134
134
|
- lib/couchrest/changes/config.rb
|
135
135
|
- lib/couchrest/changes/version.rb
|
136
|
+
- lib/couchrest/changes/observer.rb
|
136
137
|
- lib/couchrest/changes.rb
|
137
138
|
- Rakefile
|
138
139
|
- Readme.md
|
140
|
+
- test/integration/callback_test.rb
|
141
|
+
- test/setup_couch.sh
|
142
|
+
- test/config.yaml
|
143
|
+
- test/support/integration_test.rb
|
139
144
|
- test/test_helper.rb
|
140
145
|
homepage: https://leap.se
|
141
146
|
licenses: []
|
@@ -157,10 +162,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
157
162
|
version: '0'
|
158
163
|
requirements: []
|
159
164
|
rubyforge_project:
|
160
|
-
rubygems_version: 1.8.
|
165
|
+
rubygems_version: 1.8.28
|
161
166
|
signing_key:
|
162
167
|
specification_version: 3
|
163
168
|
summary: CouchRest::Changes - Observe a couch database for changes and react upon
|
164
169
|
them
|
165
170
|
test_files:
|
171
|
+
- test/integration/callback_test.rb
|
172
|
+
- test/setup_couch.sh
|
173
|
+
- test/config.yaml
|
174
|
+
- test/support/integration_test.rb
|
166
175
|
- test/test_helper.rb
|