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.
@@ -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
- class Changes
9
+ module Changes
9
10
 
10
- attr_writer :logger
11
-
12
- def initialize(db_name, options = {})
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
- class Changes
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
@@ -1,5 +1,5 @@
1
1
  module CouchRest
2
- class Changes
3
- VERSION = "0.0.5"
2
+ module Changes
3
+ VERSION = "0.1.0"
4
4
  end
5
5
  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
@@ -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
@@ -4,5 +4,8 @@ require 'minitest/autorun'
4
4
  BASE_DIR = File.expand_path('../..', __FILE__)
5
5
  $:.unshift File.expand_path('lib', BASE_DIR)
6
6
 
7
+ require 'couchrest/changes'
8
+ require 'support/integration_test'
9
+
7
10
  require 'mocha/setup'
8
11
 
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.5
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-01-02 00:00:00.000000000 Z
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.25
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