cached_counter 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d8e2382b12fc86f19c6dd734dfa801ff77f1e84d
4
+ data.tar.gz: f961dfb495177005b7f59e88e446bcc84dcf1588
5
+ SHA512:
6
+ metadata.gz: 86a9b2d91155c1b4fea65fee55d0e5815d9869081cb737b993235dd4193625356ddbe687f04a1f0574b547eaaa722a5c6da9de0ad1001832b744379e1a0dbe4d
7
+ data.tar.gz: 8fc6a71789bd035a13c6e632041ebbfceb6ec852826194c977f09f3bd351179f3c6f30087adfd49eaaeef6088cdfa3a7d7d3ba3236fe9ddedf64b858b6536556
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ *~
15
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,22 @@
1
+ bundler_args: "--without development"
2
+ language: ruby
3
+ rvm:
4
+ - 2.1.2
5
+ - 2.1.3
6
+ - ruby-head
7
+ gemfile:
8
+ - Gemfile
9
+ matrix:
10
+ allow_failures:
11
+ - rvm: ruby-head
12
+ fast_finish: true
13
+ services: memcache
14
+ before_install: gem install bundler
15
+ before_script:
16
+ - echo $PWD
17
+ - memcached -p 11212 -d
18
+ script:
19
+ - bundle exec rspec
20
+ notifications:
21
+ slack:
22
+ secure: aVCX6aZuIS4QWS2GPaAvMulnUm3L5IBtIQCRI+BMQSZcvKKwidKz/jjCaVUSK1kyjQRIBh61B/SykMTZKtxi7HV7CMklpCXJGZeJdwhDRK1ZnfjvkO0AayMYYJ0NwzHLvHaOGCxyZIvQqUT8S6HwUVpxg4wjjMzLefqCx2c/8Ek=
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in elasticsearch-model-extensions.gemspec
4
+ gemspec path: File.dirname(__FILE__)
5
+
6
+ group :test do
7
+ gem 'rspec', '~> 3.1.0'
8
+ gem 'database_cleaner'
9
+ gem 'coveralls', require: false
10
+ end
11
+
12
+ group :test, :development do
13
+ gem 'activerecord', '~> 3.2'
14
+ gem 'sqlite3'
15
+ gem 'delayed_job_active_record', '~> 4.0.1'
16
+ gem 'dalli', '~> 2.7.2'
17
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Yusuke KUOKA
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # CachedCounter
2
+
3
+ [![Build Status](https://travis-ci.org/crowdworks/cached_counter.svg?branch=master)](https://travis-ci.org/crowdworks/cached_counter)
4
+ [![Coverage Status](https://coveralls.io/repos/crowdworks/cached_counter/badge.svg?branch=master)](https://coveralls.io/r/crowdworks/cached_counter?branch=master)
5
+ [![Code Climate](https://codeclimate.com/github/crowdworks/cached_counter/badges/gpa.svg)](https://codeclimate.com/github/crowdworks/cached_counter)
6
+
7
+ Cache Counter is an instantaneous but lock-friendly implementation of the counter.
8
+
9
+ It allows to increment/decrement/get counts primarily saved in the database in a faster way.
10
+ Utilizing the cache, it can be updated without big row-locks like the ActiveRecord's update_counters,
11
+ with instantaneous and consistency unlike the updated_counters within delayed, background jobs.
12
+
13
+ ## Installation
14
+
15
+ Add this line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'cached_counter'
19
+ ```
20
+
21
+ And then execute:
22
+
23
+ $ bundle
24
+
25
+ Or install it yourself as:
26
+
27
+ $ gem install cached_counter
28
+
29
+ ## Usage
30
+
31
+ ```ruby
32
+ # When you use the counter backed by Memcached via Dalli
33
+ require 'dalli'
34
+ require' cache_store'
35
+
36
+ # Give the Cached Counter the name of the Cache Store and options you want to pass.
37
+ # The built-in DalliCacheStore is named `:dalli` and accepts the `:hosts` option which is an array of Memcached hosts
38
+ # you want Dalli connect to.
39
+ CachedCounter.cache_store :dalli, hosts: %w| 127.0.0.1 |
40
+
41
+ # We have an integer column named `num_read` in the `articles` table.
42
+ article = Article.first
43
+
44
+ # Give Cached Counter the record which has an attribute containing the initial value of the counter.
45
+ cached_counter = CachedCounter.create(record: article, attribute: :num_read)
46
+
47
+ article.num_read
48
+ #=> 0
49
+ cached_counter.value
50
+ #=> 0
51
+
52
+ # The value of the counter is updated immediately,
53
+ # and a background job is scheduled to eventually update the value of the counter persisted in the database.
54
+ cached_counter.increment
55
+
56
+ # You can see the incremented value in the cache immediately
57
+ cached_counter.value
58
+ #=> 1
59
+
60
+ # But not yet in the database
61
+ article.reload.num_read
62
+ #=> 0
63
+
64
+ # When the scheduled background job is finished,
65
+ # you can also see the incremented value in the database, too.
66
+ article.reload.num_read
67
+ #=> 1
68
+ ```
69
+
70
+
71
+ ## Contributing
72
+
73
+ 1. Fork it ( https://github.com/crowdworks/cached_counter/fork )
74
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
75
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
76
+ 4. Push to the branch (`git push origin my-new-feature`)
77
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'cached_counter/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "cached_counter"
8
+ spec.version = CachedCounter::VERSION
9
+ spec.authors = ["Yusuke KUOKA"]
10
+ spec.email = ["ykuoka@gmail.com"]
11
+ spec.summary = %q{An instantaneous but lock-friendly implementation of the counter}
12
+ spec.description = %q{Cached Counter allows to increment/decrement/get counts primarily saved in the database in a faster way. Utilizing the cache, it can be updated without big row-locks like the ActiveRecord's update_counters, with instantaneous and consistency unlike the updated_counters within delayed, background jobs}
13
+ spec.homepage = "https://github.com/crowdworks/cached_counter"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.7"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ end
@@ -0,0 +1,314 @@
1
+ require "cached_counter/version"
2
+
3
+ class CachedCounter
4
+ attr_reader :model_class, :attribute, :id
5
+
6
+ class ConcurrentCacheWriteError < StandardError; end
7
+
8
+ # @param [#call] cache_key The proc which takes the single argument of the class inheriting ActiveRecord::Base and returns a cache key
9
+ # @return [#call]
10
+ def self.cache_key(cache_key=nil)
11
+ if cache_key
12
+ @cache_key = cache_key
13
+ else
14
+ @cache_key ||= -> c { "#{c.model_class.model_name.cache_key}/#{c.attribute}/cached_counter/#{c.id}" }
15
+ end
16
+ end
17
+
18
+ # @param [#call] cache_store
19
+ # @return [CacheStore::Base]
20
+ def self.cache_store(cache_store=nil, *args, **options)
21
+ if cache_store
22
+ @cache_store = builder_for_cache_store(cache_store, *args, **options)
23
+ else
24
+ @cache_store
25
+ end
26
+ end
27
+
28
+ # @param [Proc]
29
+ def self.builder_for_cache_store(cache_store, *args, **options)
30
+ case cache_store
31
+ when Symbol
32
+ klass = cache_store_class_for_symbol(cache_store)
33
+ -> { klass.create(*args, **options) }
34
+ when Class
35
+ -> { cache_store.create(*args, **options)}
36
+ when Proc
37
+ cache_store
38
+ else
39
+ -> { cache_store }
40
+ end
41
+ end
42
+
43
+ # @param [Symbol] symbol
44
+ def self.cache_store_class_for_symbol(symbol)
45
+ const_get CacheStore.name + '::' + symbol.to_s.split('_').map(&:capitalize).join + 'CacheStore'
46
+ end
47
+
48
+ # @param [ActiveRecord::Base] record
49
+ # @param [Symbol] attribute
50
+ def self.create(record:, attribute:, cache_store: nil)
51
+ new(model_class: record.class, id: record.id, attribute: attribute, cache_store: cache_store)
52
+ end
53
+
54
+ def initialize(model_class:, id:, attribute:, cache_store: nil)
55
+ @model_class = model_class
56
+ @attribute = attribute
57
+ @id = id
58
+ @cache_store = cache_store
59
+ end
60
+
61
+ # Increment the specified attribute of the record utilizing the cache not to lock the table row as long as possible
62
+ def increment
63
+ cache_updated_successfully =
64
+ with_cache_store do |store|
65
+ begin
66
+ store.incr(cache_key) ||
67
+ # When the key doesn't exit in the cache because of cache expirations/clean-ups/restarts
68
+ store.add(cache_key, value_in_db + 1) ||
69
+ # In rare cases, the value for the key is updated by other client and we have to fail immediately, not to
70
+ # run into race-conditions.
71
+ raise(ConcurrentCacheWriteError, "Failing not to enter a race condition while writing a value for the key #{cache_key}")
72
+ rescue store.error_class => e
73
+ false
74
+ end
75
+ end
76
+
77
+ begin
78
+ if cache_updated_successfully
79
+ # When this database transaction failed afterward, we have to rollback the incrementation by decrementing
80
+ # the value in the cached.
81
+ # Without the rollback, we'll fall into an inconsistent state between the database and the cache.
82
+ on_error_rollback_by(:decrement_in_cache)
83
+
84
+ # As we have successfully incremented the value in the cache, we can rely on the cache in order to
85
+ # get/show the latest value.
86
+ # Therefore, we have no need to update the database record in realtime and we can achieve
87
+ # incrementing the counter with a little row-lock.
88
+ increment_in_db_later
89
+ else
90
+ # The cache service seems to be down, but we don't want to stop the application service.
91
+ # That's why we fall back to increment the value in the database which requires a bigger row-lock.
92
+ increment_in_db
93
+ end
94
+ rescue => e
95
+ raise e
96
+ end
97
+ end
98
+
99
+ def value
100
+ begin
101
+ cache_store.get(cache_key).try(&:to_i)
102
+ rescue cache_store.error_class => e
103
+ nil
104
+ end || value_in_db
105
+ end
106
+
107
+ # Decrement the specified attribute of the record utilizing the cache not to lock the table row as long as possible
108
+ def decrement
109
+ cache_updated_successfully =
110
+ with_cache_store do |store|
111
+ begin
112
+ store.decr(cache_key) ||
113
+ store.add(cache_key, value_in_db - 1) ||
114
+ raise(ConcurrentCacheWriteError, "Failing not to enter a race condition while writing a value for the key #{cache_key}")
115
+ rescue store.error_class => e
116
+ false
117
+ end
118
+ end
119
+
120
+ begin
121
+ if cache_updated_successfully
122
+ on_error_rollback_by(:increment_in_cache)
123
+
124
+ decrement_in_db_later
125
+ else
126
+ decrement_in_db
127
+ end
128
+ rescue => e
129
+ raise e
130
+ end
131
+ end
132
+
133
+ def value_in_db
134
+ @model_class.find(@id).send(@attribute)
135
+ end
136
+
137
+ def invalidate_cache
138
+ cache_store.delete(cache_key)
139
+ end
140
+
141
+ def increment_in_cache
142
+ with_cache_store do |d|
143
+ d.incr(cache_key)
144
+ end
145
+ end
146
+
147
+ def increment_in_db
148
+ @model_class.increment_counter(@attribute, @id)
149
+ end
150
+
151
+ def increment_in_db_later
152
+ call_method_later(:increment_in_db)
153
+ end
154
+
155
+ def decrement_in_cache
156
+ with_cache_store do |d|
157
+ d.decr(cache_key)
158
+ end
159
+ end
160
+
161
+ def decrement_in_db
162
+ @model_class.decrement_counter(@attribute, @id)
163
+ end
164
+
165
+ def decrement_in_db_later
166
+ call_method_later(:decrement_in_db)
167
+ end
168
+
169
+ # @return [String]
170
+ def cache_key
171
+ self.class.cache_key.call(self)
172
+ end
173
+
174
+ private
175
+
176
+ # @return [CacheStore::Base]
177
+ def cache_store
178
+ @cache_store ||= self.class.cache_store.call
179
+ end
180
+
181
+ def with_cache_store
182
+ yield cache_store
183
+ end
184
+
185
+ # @param [Symbol] method
186
+ def call_method_later(method)
187
+ CachedCounter::RetriedJob.new(
188
+ model_class: @model_class,
189
+ id: @id,
190
+ attribute: @attribute,
191
+ method: method
192
+ ).enqueue!
193
+ end
194
+
195
+ # @param [Symbol] method
196
+ def on_error_rollback_by(method)
197
+ listener = CachedCounter::RollbackByMethodCallListener.new(
198
+ model_class: @model_class,
199
+ id: @id,
200
+ attribute: @attribute,
201
+ method: method
202
+ )
203
+
204
+ # @see https://github.com/rails/rails/blob/v3.2.18/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb#L242
205
+ # @see https://github.com/rails/rails/blob/v4.1.4/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb#L248
206
+ @model_class.connection.add_transaction_record(listener)
207
+ end
208
+
209
+ class RetriedJob
210
+ def initialize(model_class:, id:, attribute:, method:)
211
+ @model_class = model_class
212
+ @id = id
213
+ @attribute = attribute
214
+ @method = method
215
+ end
216
+
217
+ def perform
218
+ CachedCounter.new(model_class: @model_class, id: @id, attribute: @attribute).send(@method)
219
+ end
220
+
221
+ def max_attempts
222
+ 10
223
+ end
224
+
225
+ def enqueue!
226
+ Delayed::Job.enqueue self
227
+ end
228
+ end
229
+
230
+ class RollbackByMethodCallListener
231
+ def initialize(model_class:, id:, attribute:, method:)
232
+ @model_class = model_class
233
+ @id = id
234
+ @attribute = attribute
235
+ @method = method
236
+ end
237
+
238
+ # called only when Rails is 4+
239
+ # Without this method implemented, you will see `undefined method `has_transactional_callbacks?' for #<CachedCounter::RollbackByMethodCallListener:0x007f5af749cd80>`
240
+ # when the listener is passed to `#add_transaction_record`
241
+ # @see Rails 4: https://github.com/rails/rails/blob/v4.1.4/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb#L125
242
+ def has_transactional_callbacks?
243
+ true
244
+ end
245
+
246
+ # @see Rails 3: https://github.com/rails/rails/blob/v3.2.18/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb#L372
247
+ # @see Rails 4: https://github.com/rails/rails/blob/v4.1.4/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb#L147
248
+ def committed!
249
+ nil
250
+ end
251
+
252
+ # @see ActiveRecord::Transactions::rolledback!
253
+ # @see Rails 3: https://github.com/rails/rails/blob/v3.2.18/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb#L357
254
+ # @see Rails 4: https://github.com/rails/rails/blob/v4.1.4/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb#L136
255
+ def rolledback!(force_restore_state = false)
256
+ CachedCounter.new(model_class: @model_class, id: @id, attribute: @attribute).send(@method)
257
+ end
258
+ end
259
+
260
+ module CacheStore
261
+ class Base
262
+ def incr(key); fail_with_not_implemented_error 'incr' end
263
+ def decr(key); fail_with_not_implemented_error 'decr' end
264
+ def add(key, value); fail_with_not_implemented_error 'add' end
265
+ def delete(key); fail_with_not_implemented_error 'delete' end
266
+ def error_class; fail_with_not_implemented_error 'incr' end
267
+
268
+ private
269
+
270
+ def fail_with_not_implemented_error(method_name)
271
+ fail ::CacheStore::Base::NotImplementedError, "#{self.class.name}##{method_name} must be implemented."
272
+ end
273
+
274
+ class NotImplementedError < StandardError; end
275
+ end
276
+
277
+ class DalliCacheStore < Base
278
+ # @param [Dalli::Client] dalli_client
279
+ def initialize(dalli_client)
280
+ @dalli_client = dalli_client
281
+ end
282
+
283
+ def self.create(options={})
284
+ opts = options.dup
285
+ hosts = opts.delete(:hosts)
286
+ new(Dalli::Client.new(hosts, opts))
287
+ end
288
+
289
+ def incr(key)
290
+ @dalli_client.incr(key)
291
+ end
292
+
293
+ def decr(key)
294
+ @dalli_client.decr(key)
295
+ end
296
+
297
+ def add(key, value)
298
+ @dalli_client.add(key, value, 0, raw: true)
299
+ end
300
+
301
+ def get(key)
302
+ @dalli_client.get(key)
303
+ end
304
+
305
+ def delete(key)
306
+ @dalli_client.delete(key)
307
+ end
308
+
309
+ def error_class
310
+ Dalli::RingError
311
+ end
312
+ end
313
+ end
314
+ end
@@ -0,0 +1,3 @@
1
+ class CachedCounter
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,120 @@
1
+ require 'spec_helper'
2
+
3
+ require 'dalli'
4
+
5
+ require 'cached_counter'
6
+
7
+ RSpec.describe CachedCounter do
8
+ before(:each) do
9
+ load 'setup/articles_with_delayed_jobs.rb'
10
+
11
+ CachedCounter.cache_store :dalli, hosts: %w| 127.0.0.1 |
12
+ end
13
+
14
+ after :each do
15
+ ActiveRecord::Schema.define(:version => 2) do
16
+ drop_table :delayed_jobs
17
+ drop_table :articles
18
+ end
19
+ end
20
+
21
+ let(:cache_store) { CachedCounter.cache_store.call }
22
+ let(:cached_counter) { CachedCounter.create(record: record, attribute: :num_read, cache_store: cache_store) }
23
+ let(:record) { Article.first }
24
+
25
+ subject { cached_counter }
26
+
27
+ before do
28
+ cached_counter.invalidate_cache
29
+ end
30
+
31
+ describe '#increment' do
32
+ subject { -> { cached_counter.increment } }
33
+
34
+ it { is_expected.to change { cached_counter.value }.by(1) }
35
+ it { is_expected.not_to change { record.num_read} }
36
+ it { is_expected.to change { record.reload.num_read }.by(1) }
37
+
38
+ context 'when the surrounding transaction rolled-back' do
39
+ subject { -> { transaction }}
40
+
41
+ let(:transaction) {
42
+ begin
43
+ Article.transaction do
44
+ cached_counter.increment
45
+
46
+ fail 'simulated error'
47
+ end
48
+ rescue
49
+ ;
50
+ end
51
+ }
52
+
53
+ it { is_expected.not_to change { cached_counter.value } }
54
+ it { is_expected.not_to change { record.num_read } }
55
+ it { is_expected.not_to change { record.reload.num_read } }
56
+ end
57
+
58
+ context 'when the cache is updated concurrently' do
59
+ before do
60
+ expect(cache_store).to receive(:incr).and_return(false)
61
+ expect(cache_store).to receive(:add).and_return(false)
62
+ end
63
+
64
+ subject { -> { cached_counter.increment } }
65
+
66
+ it { is_expected.to raise_error(CachedCounter::ConcurrentCacheWriteError)}
67
+ end
68
+ end
69
+
70
+ describe '#decrement' do
71
+ subject { -> { cached_counter.decrement } }
72
+
73
+ it { is_expected.to change { cached_counter.value }.by(-1) }
74
+ it { is_expected.not_to change { record.num_read} }
75
+ it { is_expected.to change { record.reload.num_read }.by(-1) }
76
+
77
+ context 'when the surrounding transaction rolled-back' do
78
+ subject { -> { transaction }}
79
+
80
+ let(:transaction) {
81
+ begin
82
+ Article.transaction do
83
+ cached_counter.decrement
84
+
85
+ fail 'simulated error'
86
+ end
87
+ rescue
88
+ ;
89
+ end
90
+ }
91
+
92
+ it { is_expected.not_to change { cached_counter.value } }
93
+ it { is_expected.not_to change { record.num_read } }
94
+ it { is_expected.not_to change { record.reload.num_read } }
95
+ end
96
+
97
+ context 'when the cache is updated concurrently' do
98
+ before do
99
+ expect(cache_store).to receive(:decr).and_return(false)
100
+ expect(cache_store).to receive(:add).and_return(false)
101
+ end
102
+
103
+ subject { -> { cached_counter.decrement } }
104
+
105
+ it { is_expected.to raise_error(CachedCounter::ConcurrentCacheWriteError)}
106
+ end
107
+ end
108
+
109
+ context 'when the cache_store is not specified' do
110
+ let(:cache_store) { nil }
111
+
112
+ describe '#increment' do
113
+ subject { -> { cached_counter.increment } }
114
+
115
+ it { is_expected.to change { cached_counter.value }.by(1) }
116
+ it { is_expected.not_to change { record.num_read} }
117
+ it { is_expected.to change { record.reload.num_read }.by(1) }
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,35 @@
1
+ load 'setup/undefine.rb'
2
+
3
+ require 'delayed_job_active_record'
4
+
5
+ ActiveRecord::Schema.define(:version => 1) do
6
+ create_table :articles do |t|
7
+ t.string :title
8
+ t.integer :num_read, :default => 0
9
+ t.datetime :created_at, :default => 'NOW()'
10
+ end
11
+
12
+ create_table :delayed_jobs, :force => true do |table|
13
+ table.integer :priority, :default => 0
14
+ table.integer :attempts, :default => 0
15
+ table.text :handler
16
+ table.text :last_error
17
+ table.datetime :run_at
18
+ table.datetime :locked_at
19
+ table.datetime :failed_at
20
+ table.string :locked_by
21
+ table.string :queue
22
+ table.timestamps
23
+ end
24
+ end
25
+
26
+ Delayed::Worker.delay_jobs = false
27
+
28
+ class ::Article < ActiveRecord::Base
29
+ end
30
+
31
+ Article.delete_all
32
+
33
+ ::Article.create! title: 'Test', num_read: 1
34
+ ::Article.create! title: 'Testing Coding'
35
+ ::Article.create! title: 'Coding'
@@ -0,0 +1,10 @@
1
+ require 'active_record'
2
+ require 'logger'
3
+
4
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => "test.db")
5
+ logger = ::Logger.new(STDERR)
6
+ logger.formatter = lambda { |s, d, p, m| "\e[2;36m#{m}\e[0m\n" }
7
+ ActiveRecord::Base.logger = logger unless ENV['QUIET']
8
+
9
+ ActiveRecord::LogSubscriber.colorize_logging = false
10
+ ActiveRecord::Migration.verbose = false
@@ -0,0 +1,7 @@
1
+ # Required to clear classes defined in `load 'setup/*.rb'` to make subsequent `load 'setup/*.rb'` to work correctly.
2
+ %i| Article Comment AuthorProfile Author Tag Book |.each do |class_name|
3
+ if Object.const_defined?(class_name)
4
+ Object.send(:remove_const, class_name)
5
+ STDERR.puts "Undefined #{class_name}"
6
+ end
7
+ end
@@ -0,0 +1,40 @@
1
+ require 'coveralls'
2
+ Coveralls.wear!
3
+
4
+ RSpec.configure do |config|
5
+ config.expect_with :rspec do |expectations|
6
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
7
+ end
8
+
9
+ config.mock_with :rspec do |mocks|
10
+ mocks.verify_partial_doubles = true
11
+ end
12
+
13
+ config.profile_examples = 10
14
+
15
+ config.order = :random
16
+
17
+ Kernel.srand config.seed
18
+
19
+ config.before :suite do
20
+ require 'setup/sqlite.rb'
21
+
22
+ require 'database_cleaner'
23
+
24
+ # https://github.com/DatabaseCleaner/database_cleaner#additional-activerecord-options-for-truncation
25
+ DatabaseCleaner.clean_with :deletion, cache_tables: false
26
+ DatabaseCleaner.strategy = :deletion
27
+ end
28
+
29
+ config.around(:each) do |example|
30
+ DatabaseCleaner.cleaning do
31
+ example.run
32
+ end
33
+ end
34
+
35
+ config.before(:each) do |s|
36
+ md = s.metadata
37
+ x = md[:example_group]
38
+ STDERR.puts "==>>> #{x[:file_path]}:#{x[:line_number]} #{md[:description_args]}"
39
+ end
40
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cached_counter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Yusuke KUOKA
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ description: Cached Counter allows to increment/decrement/get counts primarily saved
42
+ in the database in a faster way. Utilizing the cache, it can be updated without
43
+ big row-locks like the ActiveRecord's update_counters, with instantaneous and consistency
44
+ unlike the updated_counters within delayed, background jobs
45
+ email:
46
+ - ykuoka@gmail.com
47
+ executables: []
48
+ extensions: []
49
+ extra_rdoc_files: []
50
+ files:
51
+ - ".gitignore"
52
+ - ".rspec"
53
+ - ".travis.yml"
54
+ - Gemfile
55
+ - LICENSE.txt
56
+ - README.md
57
+ - Rakefile
58
+ - cached_counter.gemspec
59
+ - lib/cached_counter.rb
60
+ - lib/cached_counter/version.rb
61
+ - spec/cached_counter_spec.rb
62
+ - spec/setup/articles_with_delayed_jobs.rb
63
+ - spec/setup/sqlite.rb
64
+ - spec/setup/undefine.rb
65
+ - spec/spec_helper.rb
66
+ homepage: https://github.com/crowdworks/cached_counter
67
+ licenses:
68
+ - MIT
69
+ metadata: {}
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubyforge_project:
86
+ rubygems_version: 2.2.2
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: An instantaneous but lock-friendly implementation of the counter
90
+ test_files:
91
+ - spec/cached_counter_spec.rb
92
+ - spec/setup/articles_with_delayed_jobs.rb
93
+ - spec/setup/sqlite.rb
94
+ - spec/setup/undefine.rb
95
+ - spec/spec_helper.rb