lock_and_cache 0.1.2 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 58a808fd3a27573b09b72e166416c7c83f18c87c
4
- data.tar.gz: 935f944078a81f0801fe6efa9ab62f19e931ae75
3
+ metadata.gz: 9845e5a7f52feea98d5f059df539cd79258ba24d
4
+ data.tar.gz: 58865bd7f3a4a94516bc16e7eed6b4d26b53fba3
5
5
  SHA512:
6
- metadata.gz: b055a2757a283e0ec4eaf5529cc9eb86a5dbfad18e1ec335a0fbb475066b502d555cad457b5a596c41cb1179727b97675fb5a8fa1b5676d8632c728c7de06877
7
- data.tar.gz: ed3e65b4378637c92749d2dbf4bfa835026d52425e923aab2db898d1cb86c69487e3233334e254a6cc02125b0930e3a6dffdf8610133bccb1b3203c59104465f
6
+ metadata.gz: 57db24888a5da3addfc922b470a5cd7e1da892f5c227326413669165277d3f6aa017ab1812ca3514439fa4965c81c6627b7ecd324edb752ff0cd6b0949e78b46
7
+ data.tar.gz: 8cafb78639eb731bf7c439bfb101d65a8ec5633a2cecf68af33445e1a1224428302d1d64b2f304c13f74accc4c42e48c88e1f29975f38fb429597cfe3a3c9bbc
data/.travis.yml CHANGED
@@ -1,3 +1,5 @@
1
1
  language: ruby
2
2
  rvm:
3
3
  - 2.1.2
4
+ services:
5
+ - redis-server
data/.yardopts ADDED
@@ -0,0 +1,2 @@
1
+ --no-private
2
+ --readme README.md
data/CHANGELOG CHANGED
@@ -1,3 +1,20 @@
1
+ 1.0.1 / 2015-08-06
2
+
3
+ * Bug fixes
4
+
5
+ * Return value properly if lock was acquired but cached value immediately found
6
+
7
+ * Enhancements
8
+
9
+ * Documentation
10
+
11
+ 1.0.0 / 2015-08-05
12
+
13
+ * Enhancements
14
+
15
+ * Use Redis redlock http://redis.io/topics/distlock instead of Postgres advisory locks
16
+ * No more dependency on ActiveRecord or Postgres!
17
+
1
18
  0.1.2 / 2015-06-24
2
19
 
3
20
  * Enhancements
data/README.md CHANGED
@@ -1,31 +1,73 @@
1
1
  # LockAndCache
2
2
 
3
- TODO: Write a gem description
3
+ [![Build Status](https://travis-ci.org/seamusabshere/lock_and_cache.svg?branch=master)](https://travis-ci.org/seamusabshere/lock_and_cache)
4
+ [![Code Climate](https://codeclimate.com/github/seamusabshere/lock_and_cache/badges/gpa.svg)](https://codeclimate.com/github/seamusabshere/lock_and_cache)
5
+ [![Dependency Status](https://gemnasium.com/seamusabshere/lock_and_cache.svg)](https://gemnasium.com/seamusabshere/lock_and_cache)
6
+ [![Gem Version](https://badge.fury.io/rb/redlock.svg)](http://badge.fury.io/rb/redlock)
7
+ [![security](https://hakiri.io/github/seamusabshere/lock_and_cache/master.svg)](https://hakiri.io/github/seamusabshere/lock_and_cache/master)
8
+ [![Inline docs](http://inch-ci.org/github/seamusabshere/lock_and_cache.svg?branch=master)](http://inch-ci.org/github/seamusabshere/lock_and_cache)
4
9
 
5
- ## Wishlist
10
+ Lock and cache using redis!
6
11
 
7
- * no dep on activerecord
8
- * no dep on redis
12
+ ## Redlock locking
9
13
 
10
- ## Installation
14
+ Based on [antirez's Redlock algorithm](http://redis.io/topics/distlock).
11
15
 
12
- Add this line to your application's Gemfile:
16
+ ```ruby
17
+ LockAndCache.storage = Redis.new
18
+ ```
19
+
20
+ It will use this redis for both locking and storing cached values.
21
+
22
+ ## Convenient caching
23
+
24
+ (be sure to set up storage as above)
25
+
26
+ You put a block inside of a method:
27
+
28
+ ```ruby
29
+ class Blog
30
+ def click(arg1, arg2)
31
+ lock_and_cache(arg1, arg2, expires: 5) do
32
+ # do the work
33
+ end
34
+ end
35
+ end
36
+ ```
37
+
38
+ The key will be `{ Blog, :click, $id, $arg1, $arg2 }`. In other words, it auto-detects the class, method, object id ... and you add other args if you want.
39
+
40
+ You can change the object id easily:
13
41
 
14
42
  ```ruby
15
- gem 'lock_and_cache'
43
+ class Blog
44
+ # [...]
45
+ # if you don't define this, it will try to call #id
46
+ def lock_and_cache_key
47
+ [author, title]
48
+ end
49
+ end
16
50
  ```
17
51
 
18
- And then execute:
52
+ ## Tunables
19
53
 
20
- $ bundle
54
+ * `LockAndCache.storage=[redis]`
55
+ * `LockAndCache.lock_expires=[seconds]` default is 3 days
56
+ * `LockAndCache.lock_spin=[seconds]` (how long to wait before retrying lock) default is 0.1 seconds
57
+ * `ENV['LOCK_AND_CACHE_DEBUG']='true'` if you want some debugging output on `$stderr`
21
58
 
22
- Or install it yourself as:
59
+ ## Few dependencies
23
60
 
24
- $ gem install lock_and_cache
61
+ * [activesupport](https://rubygems.org/gems/activesupport) (come on, it's the bomb)
62
+ * [redis](https://github.com/redis/redis-rb)
63
+ * [redlock](https://github.com/leandromoreira/redlock-rb)
64
+ * [hash_digest](https://github.com/seamusabshere/hash_digest) (which requires [murmurhash3](https://github.com/funny-falcon/murmurhash3-ruby))
25
65
 
26
- ## Usage
66
+ ## Real-world usage
27
67
 
28
- TODO: Write usage instructions here
68
+ <p><a href="http://faraday.io"><img src="https://s3.amazonaws.com/photos.angel.co/startups/i/175701-a63ebd1b56a401e905963c64958204d4-medium_jpg.jpg" alt="Faraday logo"/></a></p>
69
+
70
+ We use [`lock_and_cache`](https://rubygems.org/gems/lock_and_cache) for [big data-driven marketing at Faraday](http://angel.co/faraday).
29
71
 
30
72
  ## Contributing
31
73
 
@@ -34,3 +76,7 @@ TODO: Write usage instructions here
34
76
  3. Commit your changes (`git commit -am 'Add some feature'`)
35
77
  4. Push to the branch (`git push origin my-new-feature`)
36
78
  5. Create a new Pull Request
79
+
80
+ # Copyright
81
+
82
+ Copyright 2015 Seamus Abshere
data/Rakefile CHANGED
@@ -5,3 +5,5 @@ RSpec::Core::RakeTask.new(:spec)
5
5
 
6
6
  task :default => :spec
7
7
 
8
+ require 'yard'
9
+ YARD::Rake::YardocTask.new
@@ -1,3 +1,3 @@
1
1
  module LockAndCache
2
- VERSION = '0.1.2'
2
+ VERSION = '1.0.1'
3
3
  end
@@ -1,23 +1,57 @@
1
1
  require 'lock_and_cache/version'
2
-
2
+ require 'redis'
3
+ require 'redlock'
3
4
  require 'hash_digest'
4
- require 'active_record'
5
- require 'with_advisory_lock'
5
+ require 'active_support'
6
+ require 'active_support/core_ext'
6
7
 
7
8
  module LockAndCache
8
- def LockAndCache.storage=(v)
9
- raise "only redis for now" unless v.class.to_s == 'Redis'
10
- @storage = v
9
+ DEFAULT_LOCK_EXPIRES = 60 * 60 * 24 * 3 * 1000 # 3 days in milliseconds
10
+ DEFAULT_LOCK_SPIN = 0.1
11
+
12
+ # @param redis_connection [Redis] A redis connection to be used for lock and cached value storage
13
+ def LockAndCache.storage=(redis_connection)
14
+ raise "only redis for now" unless redis_connection.class.to_s == 'Redis'
15
+ @storage = redis_connection
16
+ @lock_manager = Redlock::Client.new [redis_connection]
11
17
  end
12
18
 
19
+ # @return [Redis] The redis connection used for lock and cached value storage
13
20
  def LockAndCache.storage
14
21
  @storage
15
22
  end
16
23
 
17
- def LockAndCache.flush
18
- storage.flushdb
24
+ # @param seconds [Numeric] Lock expiry in seconds.
25
+ #
26
+ # @note Can be overridden by putting `expires:` in your call to `#lock_and_cache`
27
+ def LockAndCache.lock_expires=(seconds)
28
+ @lock_expires = seconds.to_f * 1000
29
+ end
30
+
31
+ # @return [Numeric] Lock expiry in milliseconds.
32
+ # @private
33
+ def LockAndCache.lock_expires
34
+ @lock_expires || DEFAULT_LOCK_EXPIRES
35
+ end
36
+
37
+ # @param seconds [Numeric] How long to wait before trying a lock again, in seconds
38
+ #
39
+ # @note Can be overridden by putting `lock_spin:` in your call to `#lock_and_cache`
40
+ def LockAndCache.lock_spin=(seconds)
41
+ @lock_spin = seconds.to_f
19
42
  end
20
43
 
44
+ # @private
45
+ def LockAndCache.lock_spin
46
+ @lock_spin || DEFAULT_LOCK_SPIN
47
+ end
48
+
49
+ # @private
50
+ def LockAndCache.lock_manager
51
+ @lock_manager
52
+ end
53
+
54
+ # @private
21
55
  class Key
22
56
  attr_reader :obj
23
57
  attr_reader :method_id
@@ -53,6 +87,9 @@ module LockAndCache
53
87
 
54
88
  end
55
89
 
90
+ # Clear a cache given exactly the method and exactly the same arguments
91
+ #
92
+ # @note Does not unlock.
56
93
  def lock_and_cache_clear(method_id, *key_parts)
57
94
  debug = (ENV['LOCK_AND_CACHE_DEBUG'] == 'true')
58
95
  key = LockAndCache::Key.new self, method_id, key_parts
@@ -61,13 +98,20 @@ module LockAndCache
61
98
  LockAndCache.storage.del digest
62
99
  end
63
100
 
101
+ # Lock and cache a method given key parts.
102
+ #
103
+ # @param key_parts [*] Parts that you want to include in the lock and cache key
104
+ #
105
+ # @return The cached value (possibly newly calculated).
64
106
  def lock_and_cache(*key_parts)
65
107
  raise "need a block" unless block_given?
66
108
  debug = (ENV['LOCK_AND_CACHE_DEBUG'] == 'true')
67
109
  caller[0] =~ /in `(\w+)'/
68
110
  method_id = $1 or raise "couldn't get method_id from #{kaller[0]}"
69
- options = key_parts.pop.stringify_keys if key_parts.last.is_a?(Hash)
70
- expires = options['expires'] if options
111
+ options = key_parts.last.is_a?(Hash) ? key_parts.pop.stringify_keys : {}
112
+ expires = options['expires']
113
+ lock_expires = options.fetch 'lock_expires', LockAndCache.lock_expires
114
+ lock_spin = options.fetch 'lock_spin', LockAndCache.lock_spin
71
115
  key = LockAndCache::Key.new self, method_id, key_parts
72
116
  digest = key.digest
73
117
  storage = LockAndCache.storage
@@ -76,20 +120,29 @@ module LockAndCache
76
120
  return ::Marshal.load(storage.get(digest))
77
121
  end
78
122
  Thread.exclusive { $stderr.puts "[lock_and_cache] B #{key.debug}" } if debug
79
- ActiveRecord::Base.with_advisory_lock(digest) do
123
+ retval = nil
124
+ lock_manager = LockAndCache.lock_manager
125
+ lock_digest = 'lock/' + digest
126
+ lock_info = nil
127
+ begin
128
+ until lock_info = lock_manager.lock(lock_digest, lock_expires)
129
+ sleep lock_spin
130
+ end
80
131
  Thread.exclusive { $stderr.puts "[lock_and_cache] C #{key.debug}" } if debug
81
132
  if storage.exists digest
82
- ::Marshal.load storage.get(digest)
133
+ retval = ::Marshal.load storage.get(digest)
83
134
  else
84
135
  Thread.exclusive { $stderr.puts "[lock_and_cache] D #{key.debug}" } if debug
85
- memo = yield
136
+ retval = yield
86
137
  if expires
87
- storage.setex digest, expires, ::Marshal.dump(memo)
138
+ storage.setex digest, expires, ::Marshal.dump(retval)
88
139
  else
89
- storage.set digest, ::Marshal.dump(memo)
140
+ storage.set digest, ::Marshal.dump(retval)
90
141
  end
91
- memo
92
142
  end
143
+ ensure
144
+ lock_manager.unlock lock_info
93
145
  end
146
+ retval
94
147
  end
95
148
  end
@@ -18,15 +18,15 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_runtime_dependency 'activerecord'
21
+ spec.add_runtime_dependency 'activesupport'
22
22
  spec.add_runtime_dependency 'hash_digest'
23
- spec.add_runtime_dependency 'with_advisory_lock'
23
+ spec.add_runtime_dependency 'redis'
24
+ spec.add_runtime_dependency 'redlock'
24
25
 
25
26
  spec.add_development_dependency 'pry'
26
- spec.add_development_dependency 'activesupport'
27
- spec.add_development_dependency 'bundler', '~> 1.7'
28
- spec.add_development_dependency 'pg'
27
+ spec.add_development_dependency 'bundler', '~> 1.6'
29
28
  spec.add_development_dependency 'rake', '~> 10.0'
30
29
  spec.add_development_dependency 'rspec'
31
- spec.add_development_dependency 'redis'
30
+ spec.add_development_dependency 'thread'
31
+ spec.add_development_dependency 'yard'
32
32
  end
@@ -10,13 +10,13 @@ class Foo
10
10
  end
11
11
 
12
12
  def click
13
- lock_and_cache(self) do
13
+ lock_and_cache do
14
14
  @count += 1
15
15
  end
16
16
  end
17
17
 
18
18
  def click_exp
19
- lock_and_cache(self, expires: 1, foo: :bar) do
19
+ lock_and_cache(expires: 1, foo: :bar) do
20
20
  @count_exp += 1
21
21
  end
22
22
  end
@@ -44,14 +44,12 @@ class Bar
44
44
  end
45
45
  sleep 1
46
46
  @count += 1
47
- Thread.exclusive do
48
- $clicking.delete @id
49
- end
47
+ $clicking.delete @id
50
48
  @count
51
49
  end
52
50
 
53
51
  def click
54
- lock_and_cache(self) do
52
+ lock_and_cache do
55
53
  unsafe_click
56
54
  end
57
55
  end
@@ -63,7 +61,7 @@ end
63
61
 
64
62
  describe LockAndCache do
65
63
  before do
66
- LockAndCache.flush
64
+ LockAndCache.storage.flushdb
67
65
  end
68
66
 
69
67
  it 'has a version number' do
@@ -79,7 +77,7 @@ describe LockAndCache do
79
77
 
80
78
  it "can be cleared" do
81
79
  expect(foo.click).to eq(1)
82
- foo.lock_and_cache_clear :click, foo
80
+ foo.lock_and_cache_clear :click
83
81
  expect(foo.click).to eq(2)
84
82
  end
85
83
 
@@ -93,7 +91,8 @@ describe LockAndCache do
93
91
 
94
92
  describe "locking" do
95
93
  let(:bar) { Bar.new(rand.to_s) }
96
- it "it blows up normally" do
94
+
95
+ it "it blows up normally (simple thread)" do
97
96
  a = Thread.new do
98
97
  bar.unsafe_click
99
98
  end
@@ -106,7 +105,21 @@ describe LockAndCache do
106
105
  end.to raise_error(/somebody/)
107
106
  end
108
107
 
109
- it "doesn't blow up if you lock it" do
108
+ it "it blows up (pre-existing thread pool, more reliable)" do
109
+ pool = Thread.pool 2
110
+ Thread::Pool.abort_on_exception = true
111
+ expect do
112
+ pool.process do
113
+ bar.unsafe_click
114
+ end
115
+ pool.process do
116
+ bar.unsafe_click
117
+ end
118
+ pool.shutdown
119
+ end.to raise_error(/somebody/)
120
+ end
121
+
122
+ it "doesn't blow up if you lock it (simple thread)" do
110
123
  a = Thread.new do
111
124
  bar.click
112
125
  end
@@ -117,6 +130,18 @@ describe LockAndCache do
117
130
  b.join
118
131
  end
119
132
 
133
+ it "doesn't blow up if you lock it (pre-existing thread pool, more reliable)" do
134
+ pool = Thread.pool 2
135
+ Thread::Pool.abort_on_exception = true
136
+ pool.process do
137
+ bar.click
138
+ end
139
+ pool.process do
140
+ bar.click
141
+ end
142
+ pool.shutdown
143
+ end
144
+
120
145
  end
121
146
 
122
147
  end
data/spec/spec_helper.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
2
  require 'lock_and_cache'
3
3
 
4
- ActiveRecord::Base.establish_connection adapter: 'postgresql', database: 'lock_and_cache_test'
5
-
6
4
  require 'redis'
7
5
  LockAndCache.storage = Redis.new
8
6
 
7
+ require 'thread/pool'
8
+
9
9
  require 'pry'
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lock_and_cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Seamus Abshere
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-24 00:00:00.000000000 Z
11
+ date: 2015-08-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: activerecord
14
+ name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -39,7 +39,7 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: with_advisory_lock
42
+ name: redis
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
@@ -53,13 +53,13 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: pry
56
+ name: redlock
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
- type: :development
62
+ type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
@@ -67,7 +67,7 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: activesupport
70
+ name: pry
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
@@ -86,44 +86,44 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '1.7'
89
+ version: '1.6'
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: '1.7'
96
+ version: '1.6'
97
97
  - !ruby/object:Gem::Dependency
98
- name: pg
98
+ name: rake
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - ">="
101
+ - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '0'
103
+ version: '10.0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - ">="
108
+ - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '0'
110
+ version: '10.0'
111
111
  - !ruby/object:Gem::Dependency
112
- name: rake
112
+ name: rspec
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - "~>"
115
+ - - ">="
116
116
  - !ruby/object:Gem::Version
117
- version: '10.0'
117
+ version: '0'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - "~>"
122
+ - - ">="
123
123
  - !ruby/object:Gem::Version
124
- version: '10.0'
124
+ version: '0'
125
125
  - !ruby/object:Gem::Dependency
126
- name: rspec
126
+ name: thread
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
129
  - - ">="
@@ -137,7 +137,7 @@ dependencies:
137
137
  - !ruby/object:Gem::Version
138
138
  version: '0'
139
139
  - !ruby/object:Gem::Dependency
140
- name: redis
140
+ name: yard
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
143
  - - ">="
@@ -161,6 +161,7 @@ files:
161
161
  - ".gitignore"
162
162
  - ".rspec"
163
163
  - ".travis.yml"
164
+ - ".yardopts"
164
165
  - CHANGELOG
165
166
  - Gemfile
166
167
  - LICENSE.txt