Skip to content

Commit

Permalink
Customizable :locked_at and :locked_until fields
Browse files Browse the repository at this point in the history
  • Loading branch information
dks17 committed Aug 17, 2018
1 parent 19a4fec commit a1ca391
Show file tree
Hide file tree
Showing 12 changed files with 474 additions and 320 deletions.
2 changes: 1 addition & 1 deletion .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

# Offense count: 1
Metrics/AbcSize:
Max: 18
Max: 21

# Offense count: 37
# Configuration parameters: AllowURI, URISchemes.
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### 0.3.7 (Next)

* Your contribution here.
* [#55](https://github.com/mongoid/mongoid-locker/pull/55): Customizable :locked_at and :locked_until fields - [@dks17](https://github.com/dks17).

### 0.3.6 (4/18/2018)

Expand Down
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
Document-level locking for MongoDB via Mongoid. The need arose at [Jux](https://jux.com) from multiple processes on multiple servers trying to act upon the same document and stepping on each other's toes. Mongoid-Locker is an easy way to ensure only one process can perform a certain operation on a document at a time.

[Tested](http://travis-ci.org/mongoid/mongoid-locker) against:
- MRI: `2.3.6`, `2.4.3`, `2.5.0`
- MRI: `2.3.6`, `2.4.3`, `2.5.0`
- Mongoid: `2`, `3`, `4`, `5`, `6`, `7`

See [.travis.yml](.travis.yml) for the latest test matrix.
Expand All @@ -26,6 +26,7 @@ class QueueItem
include Mongoid::Document
include Mongoid::Locker

locker
field :completed_at, :type => Time
end
```
Expand Down Expand Up @@ -57,6 +58,23 @@ Note that these locks are only enforced when using `#with_lock`, not at the data

More in-depth method documentation can be found at [rdoc.info](http://rdoc.info/github/mongoid/mongoid-locker/frames).

### Customizable :locked_at and :locked_until field names
The Locker method in your model accepts `:locked_at_field` and `:locked_until_field` options. For example, you can custom fields used by Mongoid-Locker.

```ruby
class User

include Mongoid::Document
include Mongoid::Locker

locker locked_at_field: :mongoid_locker_locked_at,
locked_until_field: { field: :mllu, as: :mongoid_locker_locked_until }
devise :database_authenticatable, :registerable, :lockable
end
```

This may be useful to avoid clashing with [Devise](https://github.com/plataformatec/devise) when it uses the same `:locked_at` field (see [Issue #26](https://github.com/mongoid/mongoid-locker/issues/26)).

## Copyright & License

Copyright (c) 2012-2018 Aidan Feldman & Contributors
Expand Down
46 changes: 31 additions & 15 deletions lib/mongoid/locker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ module ClassMethods
#
# @return [Mongoid::Criteria]
def locked
where :locked_until.gt => Time.now
where locked_until_field.gt => Time.now
end

# A scope to retrieve all unlocked documents in the collection.
#
# @return [Mongoid::Criteria]
def unlocked
any_of({ locked_until: nil }, :locked_until.lte => Time.now)
any_of({ locked_until_field => nil }, locked_until_field.lte => Time.now)
end

# Set the default lock timeout for this class. Note this only applies to new locks. Defaults to five seconds.
Expand All @@ -36,21 +36,37 @@ def lock_timeout
# default timeout of five seconds
@lock_timeout || 5
end

def locker(**fields)
options = fields.dup
default = { locked_at_field: :locked_at,
locked_until_field: :locked_until }

default.each_pair do |key, value|
if options[key].is_a?(Hash)
field_name, field_alias = options[key].fetch_values(:field, :as)
field field_name, as: field_alias, type: Time
else
field_name = options[key] || value
field field_name, type: Time
end

define_method(key) { field_name }
define_singleton_method(key) { field_name } if key == :locked_until_field
end
end
end

# @api private
def self.included(mod)
mod.extend ClassMethods

mod.field :locked_at, type: Time
mod.field :locked_until, type: Time
end

# Returns whether the document is currently locked or not.
#
# @return [Boolean] true if locked, false otherwise
def locked?
!!(locked_until && locked_until > Time.now)
!!(self[locked_until_field] && self[locked_until_field] > Time.now)
end

# Returns whether the current instance has the lock or not.
Expand Down Expand Up @@ -98,23 +114,23 @@ def acquire_lock(opts = {})
:_id => id,
'$or' => [
# not locked
{ locked_until: nil },
{ locked_until_field => nil },
# expired
{ locked_until: { '$lte' => time } }
{ locked_until_field => { '$lte' => time } }
]
},

'$set' => {
locked_at: time,
locked_until: expiration
locked_at_field => time,
locked_until_field => expiration
}

)

if locked
# document successfully updated, meaning it was locked
self.locked_at = time
self.locked_until = expiration
self[locked_at_field] = time
self[locked_until_field] = expiration
reload unless opts[:reload] == false
@has_lock = true
else
Expand Down Expand Up @@ -156,13 +172,13 @@ def unlock
{ _id: id },

'$set' => {
locked_at: nil,
locked_until: nil
locked_at_field => nil,
locked_until_field => nil
}

)

self.attributes = { locked_at: nil, locked_until: nil } unless destroyed?
self.attributes = { locked_at_field => nil, locked_until_field => nil } unless destroyed?
@has_lock = false
end
end
Expand Down
6 changes: 3 additions & 3 deletions lib/mongoid/locker/wrapper2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ def self.update(klass, query, setter)
# @param [Class] The model instance
# @return [Time] The timestamp of when the document is locked until, nil if not locked.
def self.locked_until(doc)
existing_query = { _id: doc.id, locked_until: { '$exists' => true } }
existing = doc.class.collection.find_one(existing_query, fields: { locked_until: 1 })
existing ? existing['locked_until'] : nil
existing_query = { _id: doc.id, doc.locked_until_field => { '$exists' => true } }
existing = doc.class.collection.find_one(existing_query, fields: { doc.locked_until_field => 1 })
existing ? existing[doc.locked_until_field] : nil
end
end
end
Expand Down
6 changes: 3 additions & 3 deletions lib/mongoid/locker/wrapper3.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ def self.update(klass, query, setter)
# @param [Class] The model instance
# @return [Time] The timestamp of when the document is locked until, nil if not locked.
def self.locked_until(doc)
existing_query = { _id: doc.id, locked_until: { '$exists' => true } }
existing = doc.class.where(existing_query).limit(1).only(:locked_until).first
existing ? existing.locked_until : nil
existing_query = { _id: doc.id, doc.locked_until_field => { '$exists' => true } }
existing = doc.class.where(existing_query).limit(1).only(doc.locked_until_field).first
existing ? existing[doc.locked_until_field] : nil
end
end
end
Expand Down
6 changes: 3 additions & 3 deletions lib/mongoid/locker/wrapper4.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ def self.update(klass, query, setter)
end

def self.locked_until(doc)
existing_query = { _id: doc.id, locked_until: { '$exists' => true } }
existing = doc.class.where(existing_query).limit(1).only(:locked_until).first
existing ? existing.locked_until : nil
existing_query = { _id: doc.id, doc.locked_until_field => { '$exists' => true } }
existing = doc.class.where(existing_query).limit(1).only(doc.locked_until_field).first
existing ? existing[doc.locked_until_field] : nil
end
end
end
Expand Down
6 changes: 3 additions & 3 deletions lib/mongoid/locker/wrapper5.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ def self.update(klass, query, setter)
# @param [Class] The model instance
# @return [Time] The timestamp of when the document is locked until, nil if not locked.
def self.locked_until(doc)
existing_query = { _id: doc.id, locked_until: { '$exists' => true } }
existing = doc.class.where(existing_query).limit(1).only(:locked_until).first
existing ? existing.locked_until : nil
existing_query = { _id: doc.id, doc.locked_until_field => { '$exists' => true } }
existing = doc.class.where(existing_query).limit(1).only(doc.locked_until_field).first
existing ? existing[doc.locked_until_field] : nil
end
end
end
Expand Down
6 changes: 3 additions & 3 deletions lib/mongoid/locker/wrapper6.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ def self.update(klass, query, setter)
# @param [Class] The model instance
# @return [Time] The timestamp of when the document is locked until, nil if not locked.
def self.locked_until(doc)
existing_query = { _id: doc.id, locked_until: { '$exists' => true } }
existing = doc.class.where(existing_query).limit(1).only(:locked_until).first
existing ? existing.locked_until : nil
existing_query = { _id: doc.id, doc.locked_until_field => { '$exists' => true } }
existing = doc.class.where(existing_query).limit(1).only(doc.locked_until_field).first
existing ? existing[doc.locked_until_field] : nil
end
end
end
Expand Down
Loading

0 comments on commit a1ca391

Please sign in to comment.