acts_as_lockable_by 0.1.12

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7dae7c08469a7af91f7cd46535568c5361554d1f301b7160bc3f3e6c9c6d44aa
4
+ data.tar.gz: 16875332828b1a37d7445cc6d8e027d81f012fc4e8a9e7dcc6a8b225557de6b9
5
+ SHA512:
6
+ metadata.gz: a5dc49f0a53987f3219b1451ae193e888d3218018c7028d84f661d03c62d3934de59747ba743c0737e274f232b1a538345a9aeb4efb8a522f28abd760765012f
7
+ data.tar.gz: 54d81a1c59aeae00aa79d5a4f1fe195189eaf6124381bdabdb7b68aa2681cd90b96e4eb5b46b084452aa8d272e603573111e4526257a4a05669d98f80a017337
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Tarek N. Elsamni
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,138 @@
1
+ # ActsAsLockableBy
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/acts_as_lockable_by.svg)](http://badge.fury.io/rb/acts_as_lockable_by)
4
+ [![Build Status](https://travis-ci.com/shebang-labs/acts_as_lockable_by.svg?branch=master)](https://travis-ci.com/shebang-labs/acts_as_lockable_by)
5
+
6
+ This gem was originally developed, incubated and maintained at [ABTION](https://abtion.com/). Its main goal is providing the ability to lock a resource so that no other users/lockers can access it till the lock is released or the ttl expires. It uses `redis` a shared memory space to share locks across different deployments which enables easy horizontal scalability for your ruby/rails project on multiple servers.
7
+
8
+ An example usage for this gem is when you need a blog post (resource) to be only edtiable by 1 user concurrently. So the first user to lock the blog post to himself will always have access and be able to edit it. This user will need to renew the lock before the `ttl` expires otherwise the post will be unlocked/released and any other users can lock it to themselves.
9
+
10
+ `ActsAsLockableBy` uses `redis` as a shared distributed efficient lock manager with its built-in ability to expire locks when `ttl` expires.
11
+
12
+ The `lock`, `unlock` and `renew_lock` methods in this gem are all atomic operations and running as one redis call on the redis server. Even multiple clients calling any of these methods against the same key/resource will never enter into a race condition or thread unsafety scenarios.
13
+
14
+ <!-- START doctoc generated TOC please keep comment here to allow auto update -->
15
+ <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
16
+
17
+ - [Installation](#installation)
18
+ - [Post Installation](#post-installation)
19
+ - [Usage](#usage)
20
+ - [Development](#development)
21
+ - [Contributing](#contributing)
22
+ - [License](#license)
23
+ - [Code of Conduct](#code-of-conduct)
24
+
25
+ <!-- END doctoc generated TOC please keep comment here to allow auto update -->
26
+
27
+ ## Installation
28
+
29
+ Add this line to your application's Gemfile:
30
+
31
+ ```ruby
32
+ gem 'acts_as_lockable_by'
33
+ ```
34
+
35
+ And then execute:
36
+
37
+ $ bundle
38
+
39
+ Or install it yourself as:
40
+
41
+ $ gem install acts_as_lockable_by
42
+
43
+ ### Post Installation
44
+
45
+ You need to configure the gem as follows:
46
+
47
+ ```ruby
48
+ # config/initializers/acts_as_lockable_by.rb
49
+ ActsAsLockableBy.configure do |config|
50
+ config.redis = Redis.new(url: ENV['REDIS_URL']) # redis client
51
+ config.ttl = 30.seconds # global ttl
52
+ end
53
+ ```
54
+
55
+ ## Usage
56
+
57
+ Setup
58
+
59
+ ```ruby
60
+ class User < ActiveRecord::Base
61
+ # :id is a unique identifier(attribute/method) for this object
62
+ # :ttl default to global configured ttl if not provided
63
+ acts_as_lockable_by :id, ttl: 60.seconds
64
+ end
65
+ # or if not using ActiveRecord
66
+ class Post
67
+ include ActsAsLockableBy::Lockable
68
+ acts_as_lockable_by :post_id # default to global configured ttl
69
+
70
+ def post_id
71
+ "SOME UNIQUE IDENTIFIER"
72
+ end
73
+ end
74
+
75
+ post = Post.new
76
+ ```
77
+
78
+ Lock and unlock a post
79
+
80
+ ```ruby
81
+ post.lock('Tarek Elsamni') # true
82
+ post.lock('Tarek Elsamni') # false - already locked!
83
+ post.unlock('Someone Else') # false - 'Someone Else' did not lock it!
84
+ post.unlock('Tarek Elsamni') # true - 'Tarek Elsamni' locked it!
85
+ post.unlock('Tarek Elsamni') # false - It is already unlocked!
86
+ post.lock!('Tarek Elsamni') # true
87
+ post.lock!('Tarek Elsamni') # will raise LockError - already locked!
88
+ post.unlock!('Tarek Elsamni') # true - 'Tarek Elsamni' locked it!
89
+ post.unlock!('Tarek Elsamni') # will raise UnLockError - not locked!
90
+ ```
91
+
92
+ Check if an object/resource is locked
93
+
94
+ ```ruby
95
+ post.locked? # false
96
+ post.lock('Tarek Elsamni') # true
97
+ post.locked? # true
98
+ ```
99
+
100
+ Renew a lock before its ttl expires
101
+
102
+ ```ruby
103
+ post.lock('Tarek Elsamni') # true
104
+ post.renew_lock('Someone Else') # false - 'Someone Else' did not lock it!
105
+ post.renew_lock('Tarek Elsamni') # true - 'Tarek Elsamni' locked it!
106
+ ```
107
+
108
+ Who locked an object?
109
+
110
+ ```ruby
111
+ post.lock('Tarek Elsamni') # true
112
+ post.locked? # true
113
+ post.locked_by_id # 'Tarek Elsamni'
114
+ ```
115
+
116
+ Is a class lockable?
117
+
118
+ ```ruby
119
+ Post.lockable? # true - an alias to Post.is_lockable?
120
+ ```
121
+
122
+ ## Development
123
+
124
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
125
+
126
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then open a PR with your changes against the main gem repo. If your code is passing current tests and highly covered by new tests then one of the maintainers will review it and merge. Auto releasing to a new gem version to `rubygems` is automated with [travis](http://travis-ci.org).
127
+
128
+ ## Contributing
129
+
130
+ Bug reports and pull requests are welcome on GitHub at https://github.com/shebang-labs/acts_as_lockable_by. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
131
+
132
+ ## License
133
+
134
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
135
+
136
+ ## Code of Conduct
137
+
138
+ Everyone interacting in the ActsAsLockable project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/shebang-labs/acts_as_lockable_by/blob/master/CODE_OF_CONDUCT.md).
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'acts_as_lockable'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/all'
4
+ require 'acts_as_lockable_by/version'
5
+ require 'redis'
6
+
7
+ module ActsAsLockableBy
8
+ extend ActiveSupport::Autoload
9
+ autoload :Lockable
10
+
11
+ class << self
12
+ attr_accessor :configuration
13
+ end
14
+
15
+ def self.configure
16
+ self.configuration ||= Configuration.new
17
+ yield(configuration)
18
+ end
19
+
20
+ class Configuration
21
+ attr_accessor :redis, :ttl
22
+
23
+ def initialize
24
+ @redit = Redis.new
25
+ @ttl = 30.seconds
26
+ end
27
+ end
28
+ end
29
+
30
+ ActiveSupport.on_load(:active_record) do
31
+ include ActsAsLockableBy::Lockable
32
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActsAsLockableBy
4
+ module Lockable
5
+ extend ActiveSupport::Concern
6
+
7
+ UnLockError = Class.new(StandardError)
8
+ LockError = Class.new(StandardError)
9
+
10
+ included do
11
+ class_attribute :lock_id, :ttl
12
+ end
13
+
14
+ class_methods do
15
+ def acts_as_lockable_by(
16
+ lock_id,
17
+ ttl: ActsAsLockableBy.configuration.ttl.to_i
18
+ )
19
+ self.lock_id = lock_id
20
+ self.ttl = ttl
21
+ extend ActsAsLockableBy::Lockable::SingletonMethods
22
+ include ActsAsLockableBy::Lockable::InstanceMethods
23
+ end
24
+ end
25
+
26
+ module SingletonMethods
27
+ def lockable?
28
+ true
29
+ end
30
+
31
+ # rubocop:disable Naming/PredicateName
32
+ def is_lockable?
33
+ lockable?
34
+ end
35
+ # rubocop:enable Naming/PredicateName
36
+ end
37
+
38
+ module InstanceMethods
39
+ def lock(locked_by_id)
40
+ # Set if not exist
41
+ redis.set(lock_key, locked_by_id, ex: ttl, nx: true)
42
+ end
43
+
44
+ def lock!(locked_by_id)
45
+ lock(locked_by_id) || raise(LockError)
46
+ end
47
+
48
+ def locked_by_id
49
+ redis.get(lock_key)
50
+ end
51
+
52
+ def locked?
53
+ locked_by_id.present?
54
+ end
55
+
56
+ def unlock(locked_by_id)
57
+ redis.eval(
58
+ unlock_atomic_script,
59
+ keys: [lock_key],
60
+ argv: [locked_by_id]
61
+ ) == 1
62
+ end
63
+
64
+ def unlock!(locked_by_id)
65
+ unlock(locked_by_id) || raise(UnLockError)
66
+ end
67
+
68
+ def renew_lock(locked_by_id)
69
+ # Set if only exist
70
+ redis.eval(
71
+ renew_lock_atomic_script,
72
+ keys: [lock_key],
73
+ argv: [locked_by_id, 'EX', ttl, 'XX']
74
+ ) == 'OK'
75
+ end
76
+
77
+ private
78
+
79
+ def redis
80
+ ActsAsLockableBy.configuration.redis
81
+ end
82
+
83
+ def lock_key
84
+ "ActsAsLockableBy:#{self.class.name}:#{lock_id_value}"
85
+ end
86
+
87
+ def lock_id_value
88
+ send(lock_id)
89
+ end
90
+
91
+ def unlock_atomic_script
92
+ <<LUA_SCRIPT
93
+ if redis.call("get",KEYS[1]) == ARGV[1]
94
+ then
95
+ return redis.call("del",KEYS[1])
96
+ else
97
+ return 0
98
+ end
99
+ LUA_SCRIPT
100
+ end
101
+
102
+ def renew_lock_atomic_script
103
+ <<LUA_SCRIPT
104
+ redis.log(redis.LOG_WARNING, ARGV[2])
105
+ if redis.call("get",KEYS[1]) == ARGV[1]
106
+ then
107
+ return redis.call("set", KEYS[1], ARGV[1], ARGV[2], ARGV[3], ARGV[4])
108
+ else
109
+ return 0
110
+ end
111
+ LUA_SCRIPT
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActsAsLockableBy
4
+ VERSION = '0.1.12'
5
+ end
metadata ADDED
@@ -0,0 +1,190 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acts_as_lockable_by
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.12
5
+ platform: ruby
6
+ authors:
7
+ - Tarek N. Elsamni
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-07-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '7'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '5'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '7'
33
+ - !ruby/object:Gem::Dependency
34
+ name: redis
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '4'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '4'
47
+ - !ruby/object:Gem::Dependency
48
+ name: awesome_print
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.8'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.8'
61
+ - !ruby/object:Gem::Dependency
62
+ name: bundler
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: 1.17.2
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: 1.17.2
75
+ - !ruby/object:Gem::Dependency
76
+ name: byebug
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '11.0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '11.0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: rake
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '13.0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '13.0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: rspec
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '3.0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '3.0'
117
+ - !ruby/object:Gem::Dependency
118
+ name: rubocop
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '0.52'
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '0.52'
131
+ - !ruby/object:Gem::Dependency
132
+ name: solargraph
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '0.28'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '0.28'
145
+ description: A ruby gem to atomically lock resources to prevent concurrent/multiple
146
+ lockers from accessing or editing the resource
147
+ email:
148
+ - tarek.samni@gmail.com
149
+ executables:
150
+ - console
151
+ - setup
152
+ extensions: []
153
+ extra_rdoc_files: []
154
+ files:
155
+ - LICENSE
156
+ - README.md
157
+ - bin/console
158
+ - bin/setup
159
+ - lib/acts_as_lockable_by.rb
160
+ - lib/acts_as_lockable_by/lockable.rb
161
+ - lib/acts_as_lockable_by/version.rb
162
+ homepage: https://github.com/tareksamni/acts_as_lockable_by
163
+ licenses:
164
+ - MIT
165
+ metadata:
166
+ allowed_push_host: https://rubygems.org
167
+ homepage_uri: https://github.com/tareksamni/acts_as_lockable_by
168
+ source_code_uri: https://github.com/tareksamni/acts_as_lockable_by
169
+ changelog_uri: https://github.com/tareksamni/acts_as_lockable_by/commits/master
170
+ post_install_message:
171
+ rdoc_options: []
172
+ require_paths:
173
+ - lib
174
+ required_ruby_version: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - ">="
177
+ - !ruby/object:Gem::Version
178
+ version: '0'
179
+ required_rubygems_version: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - ">="
182
+ - !ruby/object:Gem::Version
183
+ version: '0'
184
+ requirements: []
185
+ rubyforge_project:
186
+ rubygems_version: 2.7.7
187
+ signing_key:
188
+ specification_version: 4
189
+ summary: Atomically lock resources from concurrent access
190
+ test_files: []