I am trying to implement a locking in our rails application server.
REDIS.setnx works fine if I want to acquire a lock forever. But I want to acquire lock with expiry, basically I want the lock to expire after certain duration so that lock will be free to be acquired again.
From REDIS's set documentation, I see it is possible. https://redis.io/commands/set
"The command SET resource-name anystring NX EX max-lock-time is a simple way to implement a locking system with Redis."
How to implement this in ruby.
Command :
REDIS = Redis.new(host: ENV['REDIS_HOST'], port: ENV['REDIS_PORT'])
REDIS.set "key", "value", "nx", "ex", 3600
throws error:
ArgumentError: wrong number of arguments (given 5, expected 2..3)
There is another way to do that, but it requires two REDIS calls.
if(REDIS.setnx "key", "value")
REDIS.setex "key", 3600, "value"
end
This method is not preferred. I am looking to a way to acquire REDIS lock with single REDIS call in ruby. Basically "SET resource-name anystring NX EX max-lock-time" equivalent in ruby.
Thanks,
Anshul
It looks like this was added in Redis(the gem) in v3.2.2, see PR 547.
It should be used like a flag rather than as a bare string, see test.
r.set("foo", "qux", :nx => true)
Which leads me to believe you should be able to do this:
r.set("foo", "qux", :nx => true, :ex => 2.hours)
Related
I have a kind of Mongoid::Document in memory. I want to atomically inc and push in the same call. Mongoid::Criteria only exposes these operations individually.
my_model = SomeModel.find "foo"
Bad:
my_model.inc foo: 1
my_model.push bar: "b"
Good:
my_model.underlying_adapter.update "$inc" => {foo: 1}, "$push" => {bar: "b"}
The question is, how do I access that underlying adapter for a single instance of a Mongoid::Document?
You can use moped (the ruby adapter which mongoid uses) directly for this and other complex atomic operations which you want to achieve in a single query:
SomeModel.collection.find("_id" => "foo").update({
'$inc' => {"foo" => 1},
'$push' => {"bar" => "b"}})
I don't know what do you mean by atomically? timeless and without validation?
your_model.inc_field = your_model.inc_field + inc_value
your_model.push_field << pushable
your_model.timeless.save(validate: false)
P.S : Mongoid inc and push (and almost all other atomic operations) are at least 2 database hits (one read and one update).
Edit
If you want more advance query and command execution with mongoid you can use Moped (Mongoid uses Moped)
Moped driver documentation
Edit 2
It's limitation of mongoid (until current stable release - 3.1.6), refer to this issue - In version 4 of mongoid (not released yet) user can do single write operation atomically (and also chainable) Mongoid Changelog ISSUE#1344
From your code, I see you have model in memory it's at least 2 DB hit, 1 for read and 1 for write (even with chained atomic operations), Mongoid4 fixed that (1 DB hit with a single atomic operation, e.g : Band.where(name: "Depeche Mode").inc(likes: 10, followers: 20) or with document.atomically &:block syntax)
All write operations in MongoDB are atomic on the level of a single document. MongoDB Docs
I'm not a ruby expert, but maybe you can use find_and_modify
I'm having an awfully difficult time figuring out how to update a MongoDB document, using the atomic '$set' operator with Mongomatic. I'm pretty sure it's Mongo's criteria/update language I'm having troubles with, not Mongomatic, but I'm willing to be proven wrong.
The link to a gist with a standalone, runnable script is here: https://gist.github.com/3835672
I'm starting out by creating a document that looks like this:
{"videos":[{"video_id":"video1"},{"video_id":"video2"}],"_id":{"$oid": "506ddd53a114604ce3000001"}}
I can get that document using a model instantiated using Mongomatic:
video_group = VideoGroup.find_one('videos.video_id' => 'video1')
Then I'm trying to set a 'views' field, by doing this:
video_group.update!({ 'videos.video_id' => 'video1' }, '$set' => { 'videos.$.views' => 123 })
That's where Mongo blows up, with the following error:
can't append to array using string field name [$]
I know this is a very common question on StackOverflow. I understand generally that the problem is that the positional operator isn't getting any matches. But even reading through dozens of responses, I still can't figure out how to express this statement in a way that works.
Am I just starting out with the wrong data structure?
It is, in fact, a mongomatic problem. You need to pass the underlying mongo ruby driver the option {:multi => true}, as well as including your criteria with the specific _id for the update sent to mongodb instead of as part of the optional parameters. Looks like a bug in mongomatic. Here is the ruby debugger transcript that I used to find it: https://gist.github.com/3836797
Note that I made a change to the file you posted, adding the line debugger before line #41, and changing line #42-44 to this:
video_group.update!({ 'videos.video_id' => 'video1', :multi => true }, '$set' => {
'videos.$.views' => 123,
})
I'm using Mongoid (v3) to access MongoDB, and want to perform this action:
db.sessionlogs.update(
{sessionid: '12345'}, /* selection criteria */
{'$push':{rows: "new set of data"}}, /* modification */
true /* upsert */
);
This works fine in the mongo shell. It's also exactly what I want since it's a single atomic operation which is important to me as I'm going to be calling it a lot. I don't want to have to do two operations -- a fetch and then an update. I've tried a bunch of things through mongoid, but can't get it to work.
How can I get MongoID out of the way and just send this command to MongoDB? I'm guessing there's some way to do this at the Moped level, but the documentation of that library is basically non-existent.
[Answer found while writing the question...]
criteria = Sessionlogs.collection.find(:sessionid => sessionid)
criteria.upsert("$push" => {"rows" => datarow})
Here is one way to do it:
session_log = SessionLog.new(session_id: '12345')
session_log.upsert
session_log.push(:rows, "new set of data")
Or another:
SessionLog.find_or_create_by(session_id: '12345').
push(:rows, "new set of data")
#push performs an atomic $push on the field. It is explained on the
Atomic Persistence page.
(Note: the examples use UpperCamelCase and snake_case as is Ruby convention.)
Don't go down to moped just yet, you can use find and modify operation to achieve the same thing (with all the default scope and inheritance goodies)
Sample to save an edge in a graph if not existed
edge = {source_id: session[:user_id],dest_id:product._id, name: edge_name}
ProductEdge.where(edge).find_and_modify(ProductEdge.new(edge).as_document,{upsert:true})
Is there any way that I can fire a raw mongo query directly in Ruby instead of converting them to the native Ruby objects?
I went through Ruby Mongo Tutorial, but I cannot find such a method anywhere.
If it were mysql, I would have fired a query something like this.
ActiveRecord::Base.connection.execute("Select * from foo")
My mongo query is a bit large and it is properly executing in the MongoDB console. What I want is to directly execute the same inside Ruby code.
Here's a (possibly) better mini-tutorial on how to get directly into the guts of your MongoDB. This might not solve your specific problem but it should get you as far as the MongoDB version of SELECT * FROM table.
First of all, you'll want a Mongo::Connection object. If
you're using MongoMapper then you can call the connection
class method on any of your MongoMapper models to get a connection
or ask MongoMapper for it directly:
connection = YourMongoModel.connection
connection = MongoMapper.connection
Otherwise I guess you'd use the from_uri constructor to build
your own connection.
Then you need to get your hands on a database, you can do this
using the array access notation, the db method, or get
the current one straight from MongoMapper:
db = connection['database_name'] # This does not support options.
db = connection.db('database_name') # This does support options.
db = MongoMapper.database # This should be configured like
# the rest of your app.
Now you have a nice shiny Mongo::DB instance in your hands.
But, you probably want a Collection to do anything interesting
and you can get that using either array access notation or the
collection method:
collection = db['collection_name']
collection = db.collection('collection_name')
Now you have something that behaves sort of like an SQL table so
you can count how many things it has or query it using find:
cursor = collection.find(:key => 'value')
cursor = collection.find({:key => 'value'}, :fields => ['just', 'these', 'fields'])
# etc.
And now you have what you're really after: a hot out of the oven Mongo::Cursor
that points at the data you're interested in. Mongo::Cursor is
an Enumerable so you have access to all your usual iterating
friends such as each, first, map, and one of my personal
favorites, each_with_object:
a = cursor.each_with_object([]) { |x, a| a.push(mangle(x)) }
There are also command and eval methods on Mongo::DB that might do what you want.
In case you are using mongoid you will find the answer to your question here.
If you're using Mongoid 3, it provides easy access to its MongoDB driver: Moped. Here's an example of accessing some raw data without using Models to access the data:
db = Mongoid::Sessions.default
# inserting a new document
collection = db[:collection_name]
collection.insert(name: 'my new document')
# finding a document
doc = collection.find(name: 'my new document').first
# "select * from collection"
collection.find.each do |document|
puts document.inspect
end
I'm using ruby openid compliant library so I can be an openid consumer, I got the sample and when I try to start-up the service, it show errors like
/var/lib/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/session/cookie_store.rb:163:in `ensure_session_key': A key is required to write a cookie containing the session data. Use config.action_controller.session = { :key => "_myapp_session", :secret => "some secret phrase" } in config/environment.rb (ArgumentError)
from /var/lib/gems/1.8/gems/actionpack-2.3.2/lib/action_controller/session/cookie_store.rb:74:in `initialize'
any idea would be appreciated, thanks
I don't know anything about ruby but I strongly suspect you need to change these two things.
"_myapp_session"
"some secret phrase"
1 should probably be a session id (How to get this in I have no idea). 2 could in theory be left alone but it's not very secret then.
Just faced the same error. Although error message (as it often happens with ruby) is a bit messy, it says you exactly what to do: put that piece of code inside config/environment.rb.
To be specific, put it inside Rails::Initializer.run do |config| block.