Reduce the execution time of jobs of sidekiq - ruby-on-rails

I am currently working on an app which involves syncing of contacts on rails server. I am using redis server and sidekiq for performing contact syncing in the background. My database is mongodb and I am using mongoid gem as ORM. Workflow is a follows:
Contacts on the phone are passed to the rails server through app and then on the rails server, it is queued in the redis server.
Now cron job triggers sidekiq which connects to redis and completes the job.
One Job of sidekiq is as follows:
it has array of contacts(size upto 3000).
It has to process each of these contacts. By processing I mean make insert queries to DB.
Now the problem is that sidekiq takes insane amount of time to complete the job. On average it takes 50-70 sec to complete the job.
Following are the relevant files
sidekiq.yml
# Sample configuration file for Sidekiq.
# Options here can still be overridden by cmd line args.
# sidekiq -C config.yml
:verbose: true
:concurrency: 5
:logfile: ./log/sidekiq.log
:pidfile: ./tmp/pids/sidekiq.pid
:queues:
- [new_wall, 1]#6
- [contact_wall, 1]#7
- [email, 1]#5
- [gcm_chat, 1]#5
- [contact_address, 1]#7
- [backlog_contact_address, 5]
- [comment, 7]
- [default, 5]
mongoid.yml
development:
# Configure available database sessions. (required)
sessions:
# Defines the default session. (required)
default:
# Defines the name of the default database that Mongoid can connect to.
# (required).
database: "<%= ENV['DB_NAME']%>"
# Provides the hosts the default session can connect to. Must be an array
# of host:port pairs. (required)
hosts:
- "<%=ENV['MONGOD_URL']%>"
#username: "<%= ENV['DB_USERNAME']%>"
#password: "<%= ENV['DB_PASSWORD']%>"
options:
#pool: 12
# Change the default write concern. (default = { w: 1 })
# write:
# w: 1
# Change the default consistency model to primary, secondary.
# 'secondary' will send reads to secondaries, 'primary' sends everything
# to master. (default: primary)
# read: secondary_preferred
# How many times Moped should attempt to retry an operation after
# failure. (default: The number of nodes in the cluster)
# max_retries: 20
# The time in seconds that Moped should wait before retrying an
# operation on failure. (default: 0.25)
# retry_interval: 0.25
# Configure Mongoid specific options. (optional)
options:
# Includes the root model name in json serialization. (default: false)
# include_root_in_json: false
# Include the _type field in serializaion. (default: false)
# include_type_for_serialization: false
# Preload all models in development, needed when models use
# inheritance. (default: false)
# preload_models: false
# Protect id and type from mass assignment. (default: true)
# protect_sensitive_fields: true
# Raise an error when performing a #find and the document is not found.
# (default: true)
# raise_not_found_error: true
# Raise an error when defining a scope with the same name as an
# existing method. (default: false)
# scope_overwrite_exception: false
# Use Active Support's time zone in conversions. (default: true)
# use_activesupport_time_zone: true
# Ensure all times are UTC in the app side. (default: false)
# use_utc: false
test:
sessions:
default:
database: db_test
hosts:
- localhost:27017
options:
read: primary
# In the test environment we lower the retries and retry interval to
# low amounts for fast failures.
max_retries: 1
retry_interval: 0
production:
# Configure available database sessions. (required)
sessions:
# Defines the default session. (required)
default:
# Defines the name of the default database that Mongoid can connect to.
# (required).
database: "<%= ENV['DB_NAME']%>"
# Provides the hosts the default session can connect to. Must be an array
# of host:port pairs. (required)
hosts:
- "<%=ENV['MONGOD_URL']%>"
username: "<%= ENV['DB_USERNAME']%>"
password: "<%= ENV['DB_PASSWORD']%>"
pool: 10
options:
# Configure Mongoid specific options. (optional)
options:
Model.rb
def retry_save_contact_dump(c_dump_id)
c_dump = ContactDump.where(_id: c_dump_id, status: ContactDump::CONTACT_DUMP_CONS[:ERROR]).first
return false if c_dump.blank?
user = User.where(_id: c_dump.user_id).first
puts "retry_save_contact_dump"
user.save_contacts_with_name(c_dump.contacts)
c_dump.status = ContactDump::CONTACT_DUMP_CONS[:PROCESSED]
c_dump.error_msg = ""
c_dump.save
rescue => e
c_dump.status = ContactDump::CONTACT_DUMP_CONS[:CANTSYNC]
c_dump.error_msg = e.message
c_dump.save
end
def save_contacts_with_name(c_array)
m_num = Person.get_number_digest(self.mobile_number.to_s)
c_array.each do |n|
next if m_num == n["hash_mobile_number"]
p = Person.where(h_m_num: n["hash_mobile_number"]).first_or_create
save_friend(p) #if p.persisted?
p.c_names.create(name: n["name"], user_id: self.id)
end
end
ContactDump.rb
class ContactDump
include Mongoid::Document
include Mongoid::Timestamps::Created
include Mongoid::Timestamps::Updated
field :contacts, type: Array
field :status, type: Integer, default: 0
field :user_id, type: BSON::ObjectId
field :error_msg, type: String
CONTACT_DUMP_CONS = {FRESH: 0, PROCESSED: 1, ERROR: 2, CANTSYNC: 3}
end
How can I speed up the processing of jobs? I tried with permutation of increasing concurrency of sidekiq in sidekiq.yml and pool of mongoid.yml, but no help.
How do whatsApp and other messaging apps deal with contact syncing?
If some other info is required, please ask. Thanks.
EDIT: If not possible to answer this question, can anyone please suggest me other ways to sync the contacts on the rails server.

indexes to the rescue.
class ContactDump
index({status: 1})
end
class Person
index({h_m_num: 1})
end
Person might need more indexes depending on what your Person.get_number_digest does.
After adding indexes run
rake db:mongoid:create_indexes
Also, do remove the puts, you don't need that on your worker and puts is hitting your performance badly, even when you can't see the output!

Related

Rack Attack Throttling

I am trying to throttle some bots trying to brute force authentication on our production server.
This is a Rails 4 app with rack attack 6.3 and i have configured it like this:
config/initializers/rack_attack.rb
class Rack::Attack
# Throttle all requests by IP (60rpm)
#
# Key: "rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}"
throttle('req/ip', limit: 300, period: 5.minutes) do |req|
unless req.path.start_with?('/assets')
Rails.logger.error("Rack::Attack Too many requests from IP: #{req.ip}")
req.ip
end
end
### Prevent Brute-Force Attacks ###
# Throttle any POST requests by IP address
#
# Key: "rack::attack:#{Time.now.to_i/:period}:pink/posts/ip:#{req.ip}"
throttle('pink/posts/ip', limit: 1, period: 2.seconds) do |req|
if req.post?
Rails.logger.error("Rack::Attack Too many POSTS from IP: #{req.ip}")
req.ip
end
end
end
and yet i keep getting millions of requests from the same IP, am i missing something?
The docs say that rails apps use it by default so this should be the only configuration necessary to enable throttling.
So in the end both syntax like what I had and what #wscourge suggested work, the problem is that even though the official docs say that rails apps use it by default, you still need to add the following to application.rb, at least in Rails 4:
config.middleware.use Rack::Attack
From what I see in the throttling documentation syntax, the right way to do it is to execute the class method in the initializer, and not to execute it in the class definition:
config/initializers/rack_attack.rb
# Throttle all requests by IP (60rpm)
#
# Key: "rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}"
Rack::Attack.throttle('req/ip', limit: 300, period: 5.minutes) do |req|
unless req.path.start_with?('/assets')
Rails.logger.error("Rack::Attack Too many requests from IP: #{req.ip}")
req.ip
end
end
### Prevent Brute-Force Attacks ###
# Throttle any POST requests by IP address
#
# Key: "rack::attack:#{Time.now.to_i/:period}:pink/posts/ip:#{req.ip}"
Rack::Attack.throttle('pink/posts/ip', limit: 1, period: 2.seconds) do |req|
if req.post?
Rails.logger.error("Rack::Attack Too many POSTS from IP: #{req.ip}")
req.ip
end
end

Rails Sidekiq ERROR: can't find object

I have several sidekiq workers in my rails 4 app and can't figure out why the processes are failing. I am passing the User_id to the worker as a string but it can't seem to find the user. But when I search the same id in my console it finds the user. What am i doing wrong? Here is my code.
Controller.rb
def update
#user = current_user
if #user.bill.update_attributes(bill_params)
RandomWorker.perform_async(#user.id.to_s)
redirect_to users_dashboard_path, :notice => "Payment Informaton Updated"
else
...
end
end
Randomworker.rb
def perform(user_id)
user_id = user_id["$oid"] unless user_id.is_a?(String)
user = User.find(user_id)
user.process.update_report
user.process.save
end
My error comes back as
RandomWorker "575a..." Mongoid::Errors::DocumentNotFound: message: Document(s) not found for class User with id(s) 575a.... summary: When calling User.find with an id or array of ids, each parameter...
--EDIT--
My db config file is as shown:
development:
adapter: sqlite3
database: db/development.sqlite3
pool: 5
timeout: 5000
production:
adapter: sqlite3
database: db/production.sqlite3
pool: 25
timeout: 5000
And my mongoid.yml
development:
# Configure available database clients. (required)
clients:
# Defines the default client. (required)
default:
# Defines the name of the default database that Mongoid can connect to.
# (required).
database: development_db
# Provides the hosts the default client can connect to. Must be an array
# of host:port pairs. (required)
hosts:
- localhost:27017
options:
I found a solution. I had to put a monkeypatch in the sidekiq initalizer to treat the user.id as json. Apparently, mongoid struggles with this with sidekiq and although I struggled to find some documentation on it I stumbed across the answer in another unrelated question. Have to_json return a mongoid as a string
I added this in the initalizer and it seems to have fixed the issue
class BSON::ObjectId
def as_json
self.to_s
end
end
I'd advise that you include the Sidekiq::Web to view the enqueued jobs on a web interface so you can see the parameters and possibly the failures triggered.
However, this is an error I also faced a while ago too, which quite frustrated me for a while because of the number of emails I received from my error notifier(Bugsnag).
I've not found the best solution yet, but I imagine that the database was being locked for reading or some records weren't committed before attempting to use them.
Sidekiq's documentation says in your model use, after_commit :perform_sidekiq_job, on: :create.
However, I've not really tried that because I found another approach, the flip side to my new approach is that my jobs are executed much later, at times about 10minutes later.
RandomWorker.perform_in(5.seconds, user_id)
This is less likely to fail with the Sidekiq::retry option.
Read more here

deploy rails app that uses devise on heroku with mongohq

my app uses devise and mongo so i added mongohq as an addon but when it try to open my app on heroku i get this error :
An error occurred in the application and your page could not be
served. Please try again in a few moments.
If you are the application owner, check your logs for details.
heroku log :
2014-05-23T14:58:38.601715+00:00 app[web.1]: /app/vendor/bundle/ruby/2.0.0/gems/mongo-1.10.1/lib/mongo/mongo_client.rb:422:in `connect': Failed to connect to a master node at localhost:27017 (Mongo::ConnectionFailure)
2014-05-23T14:58:38.580675+00:00 app[web.1]: There is a configuration error with the current mongoid.yml.
2014-05-23T14:58:38.580684+00:00 app[web.1]:
2014-05-23T14:58:38.580686+00:00 app[web.1]: Problem:
2014-05-23T14:58:38.580688+00:00 app[web.1]: No sessions configuration provided.
2014-05-23T14:58:38.580690+00:00 app[web.1]: Summary:
2014-05-23T14:58:38.580692+00:00 app[web.1]: Mongoid's configuration requires that you provide details about each session that can be connected to, and requires in the sessions config at least 1 default session to exist.
2014-05-23T14:58:38.580693+00:00 app[web.1]: Resolution:
2014-05-23T14:58:38.580695+00:00 app[web.1]: Double check your mongoid.yml to make sure that you have a top-level sessions key with at least 1 default session configuration for it. You can regenerate a new mongoid.yml for assistance via `rails g mongoid:config`.
2014-05-23T14:58:38.580697+00:00 app[web.1]:
2014-05-23T14:58:38.580698+00:00 app[web.1]: Example:
2014-05-23T14:58:38.580700+00:00 app[web.1]:   development:
2014-05-23T14:58:38.580701+00:00 app[web.1]:     sessions:
2014-05-23T14:58:38.580703+00:00 app[web.1]:       default:
2014-05-23T14:58:38.580704+00:00 app[web.1]:         database: mongoid_dev
here's my mongoid.yml:
development:
# Configure available database sessions. (required)
sessions:
# Defines the default session. (required)
default:
# Defines the name of the default database that Mongoid can connect to.
# (required).
database: healthy_grocery_development
# Provides the hosts the default session can connect to. Must be an array
# of host:port pairs. (required)
hosts:
- localhost:27017
options:
# Change whether the session persists in safe mode by default.
# (default: false)
# safe: false
# Change the default consistency model to :eventual or :strong.
# :eventual will send reads to secondaries, :strong sends everything
# to master. (default: :eventual)
# consistency: :eventual
# How many times Moped should attempt to retry an operation after
# failure. (default: 30)
# max_retries: 30
# The time in seconds that Moped should wait before retrying an
# operation on failure. (default: 1)
# retry_interval: 1
# Configure Mongoid specific options. (optional)
options:
# Configuration for whether or not to allow access to fields that do
# not have a field definition on the model. (default: true)
# allow_dynamic_fields: true
# Enable the identity map, needed for eager loading. (default: false)
# identity_map_enabled: false
# Includes the root model name in json serialization. (default: false)
# include_root_in_json: false
# Include the _type field in serializaion. (default: false)
# include_type_for_serialization: false
# Preload all models in development, needed when models use
# inheritance. (default: false)
# preload_models: false
# Protect id and type from mass assignment. (default: true)
# protect_sensitive_fields: true
# Raise an error when performing a #find and the document is not found.
# (default: true)
# raise_not_found_error: true
# Raise an error when defining a scope with the same name as an
# existing method. (default: false)
# scope_overwrite_exception: false
# Skip the database version check, used when connecting to a db without
# admin access. (default: false)
# skip_version_check: false
# User Active Support's time zone in conversions. (default: true)
# use_activesupport_time_zone: true
# Ensure all times are UTC in the app side. (default: false)
# use_utc: false
test:
sessions:
default:
database: healthy_grocery_test
hosts:
- localhost:27017
options:
consistency: :strong
# In the test environment we lower the retries and retry interval to
# low amounts for fast failures.
max_retries: 1
retry_interval: 0
production:
sessions:
default:
uri: <%= ENV['MONGOHQ_URL'] %>
options:
skip_version_check: true
safe: true

Mongodb in rails:Could not connect to a primary node for a replica

I am new to rails and mongodb. i have generated mongod model in my rails project name as Forum
when i am trying to add record using following commands in rails console
f = Forum.new
f.topic_name = "my_topic"
f.save
I am getting following error
Moped::Errors::ConnectionFailure: Could not connect to a primary node for replic
a set #<Moped::Cluster:25470936 #seeds=[<Moped::Node resolved_address=nil>]>
I am running mongod server on port 27017
following is my mongoid.yml file
development:
# Configure available database sessions. (required)
sessions:
# Defines the default session. (required)
default:
# Defines the name of the default database that Mongoid can connect to.
# (required).
database: local
# Provides the hosts the default session can connect to. Must be an array
# of host:port pairs. (required)
hosts:
- localhost:27017
options:
# Change the default write concern. (default = { w: 1 })
# write:
# w: 1
# Change the default consistency model to primary, secondary.
# 'secondary' will send reads to secondaries, 'primary' sends everything
# to master. (default: primary)
# read: secondary_preferred
# How many times Moped should attempt to retry an operation after
# failure. (default: 30)
# max_retries: 30
# The time in seconds that Moped should wait before retrying an
# operation on failure. (default: 1)
# retry_interval: 1
# Configure Mongoid specific options. (optional)
options:
# Includes the root model name in json serialization. (default: false)
# include_root_in_json: false
# Include the _type field in serializaion. (default: false)
# include_type_for_serialization: false
# Preload all models in development, needed when models use
# inheritance. (default: false)
# preload_models: false
# Protect id and type from mass assignment. (default: true)
# protect_sensitive_fields: true
# Raise an error when performing a #find and the document is not found.
# (default: true)
# raise_not_found_error: true
# Raise an error when defining a scope with the same name as an
# existing method. (default: false)
# scope_overwrite_exception: false
# Use Active Support's time zone in conversions. (default: true)
# use_activesupport_time_zone: true
# Ensure all times are UTC in the app side. (default: false)
# use_utc: false
test:
sessions:
default:
database: local
hosts:
- localhost:27017
options:
read: primary
# In the test environment we lower the retries and retry interval to
# low amounts for fast failures.
max_retries: 1
retry_interval: 0
what to do??
review the localhost map in your hosts file or try hosts as 0.0.0.0:27017.
Information about host file: http://en.wikipedia.org/wiki/Hosts_(file)

Mongoid::Errors::DocumentNotFound raise_not_found_error

* updated at the bottom *
When looking for user that doesnt exist, I am getting:
Mongoid::Errors::DocumentNotFound in UsersController#show
Problem:
Document(s) not found for class User with id(s) 22. Summary: When
calling User.find with an id or array of ids, each parameter must
match a document in the database or this error will be raised. The
search was for the id(s): 22 ... (1 total) and the following ids were
not found: 22. Resolution: Search for an id that is in the database or
set the Mongoid.raise_not_found_error configuration option to false,
which will cause a nil to be returned instead of raising this error
when searching for a single id, or only the matched documents when
searching for multiples.
However I am setting raise_not_found_error to false
mongoid.yml
development:
adapter: 'mongoid'
# Configure available database sessions. (required)
sessions:
# Defines the default session. (required)
default:
# Defines the name of the default database that Mongoid can connect to.
# (required).
database: blog_development
# Provides the hosts the default session can connect to. Must be an array
# of host:port pairs. (required)
hosts:
- localhost:27017
options:
allow_dynamic_fields: false
identity_map_enabled: true
include_root_in_json: true
include_type_for_serialization: true
# Note this can also be true if you want to preload everything, but this is
# almost never necessary. Most of the time set this to false.
preload_models:
- Canvas
- Browser
- Firefox
scope_overwrite_exception: true
raise_not_found_error: false
skip_version_check: false
use_activesupport_time_zone: false
use_utc: true
# Configure Mongoid specific options. (optional)
options:
# Enable the identity map, needed for eager loading. (default: false)
# identity_map_enabled: false
# Includes the root model name in json serialization. (default: false)
# include_root_in_json: false
# Include the _type field in serializaion. (default: false)
# include_type_for_serialization: false
# Preload all models in development, needed when models use
# inheritance. (default: false)
# preload_models: false
# Protect id and type from mass assignment. (default: true)
# protect_sensitive_fields: true
# Raise an error when performing a #find and the document is not found.
# (default: true)
raise_not_found_error: false
# Raise an error when defining a scope with the same name as an
# existing method. (default: false)
scope_overwrite_exception: false
# Skip the database version check, used when connecting to a db without
# admin access. (default: false)
# skip_version_check: false
# Use Active Support's time zone in conversions. (default: true)
# use_activesupport_time_zone: true
# Ensure all times are UTC in the app side. (default: false)
# use_utc: false
test:
sessions:
default:
database: blog_test
hosts:
- localhost:27017
options:
consistency: :strong
# In the test environment we lower the retries and retry interval to
# low amounts for fast failures.
max_retries: 1
retry_interval: 0
Controller
# GET /users/1
# GET /users/1.json
def show
#user = User.find(params[:id])
render json: #user
end
* UPDATE **
fixed a null response (not in a json format) going out by doing this:
def show
#user = User.find(params[:id])
if #user.nil?
#user = []
end
render json: #user
end
your structure of yml is wrong
has to be -
development:
sessions:
options:
#raise_not_found_error has to be not here but see below
options: #strictly 2 spaces before
raise_not_found_error: false #strictly 4 spaces before not 6
so, raise_not_found_error parameter has to be child of development>options, not development>sessions>options
For me even correct indenting didn't work, what did is to create an initializer file called mongoid.rb in config/initializers/ and put this into it
Mongoid.raise_not_found_error = false
To save someone few minutes, if you are still having a problem and you are sure your mongoid.yml configs are correct, try to stop spring server as it seems to do a lot of caching!
$ spring stop

Resources