New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Lock#try_lock should not raise exception? #160
Comments
Hi @niv, It could, but that doesn't make it better I think. If the same thread calls |
Ah, you might (not) enjoy this. I'm trying to reuse/extend NoBrainer::Lock for my own nefarious purposes. I basically want to hold locks alive from http requests; for this, I've written this piece of code:
It's not really tested or anything, just an experiment (I probably need to check for expiry too). I want to be able to reinstance a lock including the lock key (based on a secret the locker knows). In the actual service, I do this:
Not sure if that is the best way to go about it - I probably need something like Lock#locked_by_me?. I can certainly understand that calling try_lock twice from the same context isn't good. |
I don't understand what you are trying to accomplish.
Can you clarify what that means? Can you describe the behavior of what you'd like? |
I want to add a locking mechanism to NoBrainer models, so that only one authenticated client can make changes to them (protected with a simple guard in the API handler). Those locks need to expire after a time automatically, and be unlockable (by the authenticated client) before that. I was going to roll my own, but I figured I could use NoBrainer::Lock the same way with minimum changes. |
Example usecase: (client 1) POST /model/1/lock ? duration=5min -> locked |
I see. Let me come up with something. |
Thank you very much! |
I've pushed an implementation of
Here's a usage that you might like for your usecase: class SomeController
def ownership_lock(model)
NoBrainer::ReentrantLock.new("ownership:models:#{model.id}",
:instance_token => current_user.id,
:timeout => 0)
# specifying timeout = 0 will make all the call to lock/synchronize raise if
# the lock is not available.
end
def action_lock
# careful, calling try_lock multiple times will require multiple unlock calls.
if ownership_lock(model).lock
...
else
...
end
end
def action_unlock
ownership_lock(model).unlock
end
def update_model
ownership_lock(model).synchronize do
model.update(...)
end
end
end I will write the documentation later, but this should help you getting where you need to get. |
I'll try it ASAP. Thank you for your outstanding timing and help. |
My pleasure :) Let me know how it goes |
Tried it really quickly and found a couple of oddities.
Also, I think in your example you meant user.try_lock, instead of lock, right? ReentrantLock#lock always errors for me with "lock unavailable". Edit: #lock errors because of timeout: 0, since it never reaches inside the while loop that hits try_lock. |
Oops yes that's a bug. I'll fix that in ~1hour. Sorry about that |
Don't worry about it! It's late over here so I will revisit after sleep at the soonest. Cheers. |
alright, that should be fixed :) :) |
Also, I made a choice about the semantics which is debatable. Say that you hold an instance of the lock its count is 5 or whatever. You let the lock expire (maybe the machine died?). The next day, with the same lock instance, you proceed to lock. Should the counter be 6, or 1?. |
This is looking good! And for what it's worth, I agree with your reasoning that expired locks should reset to zero. Two minor things though:
|
class SomeController
def action_lock
ownership_lock(model).synchronize do
model.reload
if model.owner != current_user
model.update(:owner => current_user)
ownership_lock(model).lock
end
end
end
def action_unlock
ownership_lock(model).synchronize do
model.reload
if model.owner == current_user
model.update(:owner => nil)
ownership_lock(model).unlock
end
end
end
def update_model
ownership_lock(model).synchronize do
model.update(...)
end
end
end |
Here's how my current code looks like. @character and @server are both set someplace else, @server being the authenticated user, @character the current resource. POSTing to /lock simply locks @character to the current @server, and similarily /unlock will free it for other users to acquire. The other side repeatedly calls /lock to refresh the lock until such time it no longer needs it and calls /unlock (or crashes and the lock expires by itself). I don't understand how this would give a race condition though. I have two (workaround) rql blocks in my code to ... (on @character)
def reentrant_lock token
NoBrainer::ReentrantLock.new "reentrant_lock:#{self.class.name.to_s}:#{self.id}",
instance_token: token,
timeout: 0
end
...
post :lock do
lock = @character.reentrant_lock(@server.api_key)
r = if lock.try_lock expire: 5.minutes.to_i
NoBrainer.run {
NoBrainer::ReentrantLock.rql_table.filter(key: lock.key).
pluck(:expires_at)
}.first
else
false
end
{
"lock": r
}
end
post :unlock do
lock = @character.reentrant_lock(@server.api_key)
NoBrainer.run {
NoBrainer::ReentrantLock.rql_table.filter {|row|
row[:key].eq(lock.key) && row[:lock_count].gt(0)
}.update(lock_count: 1)
}
{
"unlock": (begin
lock.unlock
true
rescue NoBrainer::Error::LostLock, NoBrainer::Error::LockUnavailable
false
end)
}
end Sorry if I'm being dense and missing somthing. |
You are not showing the code related to the character updates, so it's hard to show where these races could be (the Other than that, the unlock update code you show has issues: |
You're right, i missed that completely. I'll rework and get back to you tomorrow. |
Actually, my code doesn't work either. It would work if "Say that you hold an instance of the lock its count is 5 or whatever. You let the lock expire (maybe the machine died?). The next day, with the same lock instance, you proceed to lock. Should the counter be 6, or 1?." would return 6, and not 1. That's because there is no way to tell if a lock was recovered or not. When it's recovered, the model.owner should be set to nil. |
I've switched back to the non-reset of the counter when recovering a lock from the same instance. It makes coding easier (no need to deal with special recovery code for most use cases): 7f166a9 |
Hi,
I'm having some doubts about this line:
https://github.com/nviennot/nobrainer/blob/9815a2da3f700cdc310970b7d66ce8cf83bd8390/lib/no_brainer/lock.rb#L59
It throws an exception when calling try_lock from the same lock twice. Shouldn't it just return false/true depending on if the current caller already holds the lock?
Cheers
The text was updated successfully, but these errors were encountered: