lock_and_cache 0.1.2 → 1.0.1

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