couchrest_changes 0.0.5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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