Rails cache counter - ruby-on-rails

I have a simple Ruby method meant to throttle some execution.
MAX_REQUESTS = 60
# per
TIME_WINDOW = 1.minute
def throttle
cache_key = "#{request.ip}_count"
count = Rails.cache.fetch(cache_key, expires_in: TIME_WINDOW.to_i) { 0 }
if count.to_i >= MAX_REQUESTS
render json: { message: 'Too many requests.' }, status: 429
return
end
Rails.cache.increment(cache_key)
true
end
After some testing I've found that cache_key never invalidates.
I investigated with binding.pry and found the issue:
[35] pry(#<Refinery::ApiReferences::Admin::ApiHostsController>)> Rails.cache.write(cache_key, count += 1, expires_in: 60, raw: true)
=> true
[36] pry(#<Refinery::ApiReferences::Admin::ApiHostsController>)> Rails.cache.send(:read_entry, cache_key, {})
=> #<ActiveSupport::Cache::Entry:0x007fff1e34c978 #created_at=1495736935.0091069, #expires_in=60.0, #value=11>
[37] pry(#<Refinery::ApiReferences::Admin::ApiHostsController>)> Rails.cache.increment(cache_key)
=> 12
[38] pry(#<Refinery::ApiReferences::Admin::ApiHostsController>)> Rails.cache.send(:read_entry, cache_key, {})
=> #<ActiveSupport::Cache::Entry:0x007fff1ee105a8 #created_at=1495736965.540865, #expires_in=nil, #value=12>
So, increment is wiping out the expires_in value and changing the created_at, regular writes will do the same thing.
How do I prevent this? I just want to update the value for a given cache key.
UPDATE
Per suggestion I tried:
MAX_REQUESTS = 60
# per
TIME_WINDOW = 1.minute
def throttle
cache_key = "#{request.ip}_count"
count = Rails.cache.fetch(cache_key, expires_in: TIME_WINDOW.to_i, raw: true) { 0 }
if count.to_i >= MAX_REQUESTS
render json: { message: 'Too many requests.' }, status: 429
return
end
Rails.cache.increment(cache_key)
true
end
No effect. Cache does not expire.

Here's a "solution," I won't mark it correct because surely this isn't necessary?
MAX_REQUESTS = 60
# per
TIME_WINDOW = 1.minute
def throttle
count_cache_key = "#{request.ip}_count"
window_cache_key = "#{request.ip}_window"
window = Rails.cache.fetch(window_cache_key) { (Time.zone.now + TIME_WINDOW).to_i }
if Time.zone.now.to_i >= window
Rails.cache.write(window_cache_key, (Time.zone.now + TIME_WINDOW).to_i)
Rails.cache.write(count_cache_key, 1)
end
count = Rails.cache.read(count_cache_key) || 0
if count.to_i >= MAX_REQUESTS
render json: { message: 'Too many requests.' }, status: 429
return
end
Rails.cache.write(count_cache_key, count + 1)
true
end

Incrementing a raw value (with raw: true option) in Rails cache works exactly the way you desire, i.e. it updates only the value, not the expiration time. However, when debugging this, you cannot rely on the output of read_entry very much as this does not correspond fully with the raw value stored in cache, because the cache store does not give back the expiry time when storing just the raw value.
That is why, normally (without the raw) option, Rails does not store just the raw value, but it creates a cache Entry object which, besides the value, holds additional data, such as the expiry time. Then it serializes this object and saves it to the cache store. Upon reading the value back, it de-serializes the object and still has access to all info, including the expiry time.
However, as you cannot increment a serialized object, you need to store a raw value instead, i.e. use the raw: true option. This makes Rails store directly the value and pass the expiry time as param to the cache store write method (without the possibility to read it back from the store).
So, to sum up, you must use raw: true when caching a value for incrementing and the expiry time will be normally preserved in the cache store. See the following test (done on the mem_cache_store store):
# cache_test.rb
cache_key = "key"
puts "setting..."
Rails.cache.fetch(cache_key, expires_in: 3.seconds, raw: true) { 1 }
puts "#{Time.now} cached value: #{Rails.cache.read(cache_key)}"
sleep(2)
puts "#{Time.now} still cached: #{Rails.cache.read(cache_key)}"
puts "#{Time.now} incrementing..."
Rails.cache.increment(cache_key)
puts "#{Time.now} incremented value: #{Rails.cache.read(cache_key)}"
sleep(1)
puts "#{Time.now} gone!: #{Rails.cache.read(cache_key).inspect}"
When running this, you'll get:
$ rails runner cache_test.rb
Running via Spring preloader in process 31666
setting...
2017-05-25 22:15:26 +0200 cached value: 1
2017-05-25 22:15:28 +0200 still cached: 1
2017-05-25 22:15:28 +0200 incrementing...
2017-05-25 22:15:28 +0200 incremented value: 2
2017-05-25 22:15:29 +0200 gone!: nil
As you can see, the value has been incremented without resetting the expiry time.
Update: I set up a minimal test for you code, though not run through a real controller but only as a script. I made only 4 small changes to the throttle code in your OP:
lowered the time window
changed render to a simple puts
used only a single key as if requests came from a single IP address
print the incremented value
The script:
# chache_test2.rb
MAX_REQUESTS = 60
# per
#TIME_WINDOW = 1.minute
TIME_WINDOW = 3.seconds
def throttle
#cache_key = "#{request.ip}_count"
cache_key = "127.0.0.1_count"
count = Rails.cache.fetch(cache_key, expires_in: TIME_WINDOW.to_i, raw: true) { 0 }
if count.to_i >= MAX_REQUESTS
#render json: { message: 'Too many requests.' }, status: 429
puts "too many requests"
return
end
puts Rails.cache.increment(cache_key)
true
end
62.times do |i|
throttle
end
sleep(3)
throttle
The run prints the following:
$ rails runner cache_test2.rb
Running via Spring preloader in process 32589
2017-05-26 06:11:26 +0200 1
2017-05-26 06:11:26 +0200 2
2017-05-26 06:11:26 +0200 3
2017-05-26 06:11:26 +0200 4
...
2017-05-26 06:11:26 +0200 58
2017-05-26 06:11:26 +0200 59
2017-05-26 06:11:26 +0200 60
2017-05-26 06:11:26 +0200 too many requests
2017-05-26 06:11:26 +0200 too many requests
2017-05-26 06:11:29 +0200 1
Perhaps you don't have caching configured in development at all? I recommend testing this in the memcached store, which is the most preferred cache store in production environment. In development, you need to explicitly switch it on:
# config/environemnts/development.rb
config.cache_store = :mem_cache_store
Also, if you are running a recent Rails 5.x version, you may need to run the rails dev:cache command which creates the tmp/caching-dev.txt file that is used in the development config to actually enable caching in development env.

Related

Rails 5 - Sidekiq worker shows job done but nothing happens

I'm using Sidekiq for delayed jobs with sidekiq-status and sidekiq-ent gems. I've created a worker which is reponsible to update minor status to false when user is adult and has minor: true. This worker should be fired every day at midnight ET. Like below:
#initializers/sidekiq.rb
config.periodic do |mgr|
# every day between midnight 0 5 * * *
mgr.register("0 5 * * *", MinorWorker)
end
#app/workers/minor_worker.rb
class MinorWorker
include Sidekiq::Worker
def perform
User.adults.where(minor: true).remove_minor_status
rescue => e
Rails.logger.error("Unable to update minor field. Exception: #{e.message} : #{e.backtrace.join('\n')}")
end
end
#models/user.rb
class User < ApplicationRecord
scope :adults, -> { where('date_of_birth <= ?', 18.years.ago) }
def self.remove_minor_status
update(minor: false)
end
end
No I want to check this on my local machine - to do so I'm using gem 'timecop' to timetravel:
#application.rb
config.time_zone = 'Eastern Time (US & Canada)'
#config/environments/development.rb
config.after_initialize do
t = Time.local(2021, 12, 21, 23, 59, 0)
Timecop.travel(t)
end
After firing up sidekiq by bundle exec sidekiq and bundle exec rails s I'm waiting a minute and I see that worker shows up:
2021-12-21T22:59:00.130Z 25711 TID-ovvzr9828 INFO: Managing 3 periodic jobs
2021-12-21T23:00:00.009Z 25711 TID-ovw69k4ao INFO: Enqueued periodic job SettlementWorker with JID ddab15264f81e0b417e7dd83 for 2021-12-22 00:00:00 +0100
2021-12-21T23:00:00.011Z 25711 TID-ovw69k4ao INFO: Enqueued periodic job MinorWorker with JID 0bcd6b76d6ee4ff9e7850b35 for 2021-12-22 00:00:00 +0100
But it didn't do anything, the user's minor status is still set to minor: true:
2.4.5 :002 > User.last.date_of_birth
=> Mon, 22 Dec 2003
2.4.5 :001 > User.last.minor
=> true
Did I miss something?
EDIT
I have to add that when I'm trying to call this worker on rails c everything works well. I've got even a RSpec test which also passes:
RSpec.describe MinorWorker, type: :worker do
subject(:perform) { described_class.new.perform }
context 'when User has minor status' do
let(:user1) { create(:user, minor: true) }
it 'removes minor status' do
expect { perform }.to change { user1.reload.minor }.from(true).to(false)
end
context 'when user is adult' do
let(:registrant2) { create(:registrant) }
it 'not change minor status' do
expect(registrant2.reload.minor).to eq(false)
end
end
end
end
Since this is the class method update won't work
def self.remove_minor_status
update(minor: false)
end
Make use of #update_all
def self.remove_minor_status
update_all(minor: false)
end
Also, I think it's best practice to have some test cases to ensure the working of the methods.
As of now you can try this method from rails console and verify if they actually work
test "update minor status" do
user = User.create(date_of_birth: 19.years.ago, minor: true)
User.adults.where(minor: true).remove_minor_status
assert_equal user.reload.minor, false
end
I think you need to either do update_all or update each record by itself, like this:
User.adults.where(minor: true).update_all(minor: false)
or
class MinorWorker
include Sidekiq::Worker
def perform
users = User.adults.where(minor: true)
users.each { |user| user.remove_minor_status }
rescue => e
Rails.logger.error("Unable to update minor field. Exception: #{e.message} : #{e.backtrace.join('\n')}")
end
end
You may also want to consider changing update to update! so it throws an error if failing to be caught by your rescue in the job:
def self.remove_minor_status
update!(minor: false)
end

Rake task errors with: JSON::ParserError: 765: unexpected token at '' but works fine in rails console

I have a rake task which loops over pages of card game database and checks for the cards in each deck. Until recently this was working fine (it's checked 34000 pages of 25 decks each no problem) but recently this has stopped working when I run the rake task and I get the error:
JSON::ParserError: 765: unexpected token at ''
In order to debug this I have tried running each line of the get request and json parse manually in the rails console and it works fine every time. Weirder still I have installed pry and it works every time I go through the json parse manually with pry (takes ages though).
Here is the rake task:
desc "Create Cards"
require 'net/http'
require 'json'
task :create_cards => :environment do
# Get the total number of pages of decks
uri = URI("https://www.keyforgegame.com/api/decks/")
response = Net::HTTP.get(URI(uri))
json = JSON.parse(response)
deck_count = json["count"]
# Set variables
page_number = 1
page_size = 25 # 25 is the max page size
page_limit = deck_count / 25
card_list = Card.where(is_maverick: false)
# Updates Card List (non-mavericks) - there are 740 cards so we stop when we have that many
# example uri: https://www.keyforgegame.com/api/decks/?page=1&page_size=30&search=&links=cards
puts "Updating Card List..."
until page_number > page_limit || Card.where(is_maverick: false).length == 740
uri = URI("https://www.keyforgegame.com/api/decks/?page=#{page_number}&page_size=#{page_size}&search=&links=cards")
response = Net::HTTP.get(URI(uri))
json = JSON.parse(response) # task errors here!
cards = json["_linked"]["cards"]
cards.each do |card|
unless Card.exists?(:card_id => card["id"])
Card.create({
card_id: card["id"],
amber: card["amber"],
card_number: card["card_number"],
card_text: card["card_text"],
card_title: card["card_title"],
card_type: card["card_type"],
expansion: card["expansion"],
flavor_text: card["flavor_text"],
front_image: card["front_image"],
house: card["house"],
is_maverick: card["is_maverick"],
power: card["power"],
rarity: card["rarity"],
traits: card["traits"],
})
end
end
puts "#{page_number}/#{page_limit} - Cards: #{Card.where(is_maverick: false).length}"
page_number = (page_number + 1)
end
end
The first json parse where it gets the total number of pages of decks works okay. It's the json parse in the until block that is failing (I've marked the line with a comment to that effect).
As I say, if I try this in the console it works fine and I can parse the json without error, literally copying and pasting the lines from the file into the rails console.
Since you're looping over an api, it's possible there are rate limits. Public APIs normally have per second rate limits. You could try adding a sleep to slow down your requests, not sure how many your making per second. I tested with a simple loop and looks like response returns an empty string if you hit the api too fast.
url='https://www.keyforgegame.com/api/decks/?page=1&page_size=30&search=&links=cards'
uri = URI(url)
i = 1
1000.times do
puts i.to_s
i += 1
response = Net::HTTP.get(URI(uri))
begin
j = JSON.parse(response)
rescue
puts response
#= ""
end
end
I played with this until the loop stopped returning empty string after the 3rd request and got it to work with sleep 5 inside each loop, so you can probably add as the first line inside your loop. But you should probably add error handling to your rake task in case you encounter any other API errors.
So for now you can probably just do this
until page_number > page_limit || Card.where(is_maverick: false).length == 740
sleep 5
# rest of your loop code, maybe add a rescue like I've shown
end

Threads in rails 4.1.9 (using Jruby 9.0.0.0 preview)

So I have a module that is trying to make use of multiple threads, one per site. It then trying to do a request on each thread. From there I am trying to say:
Create a thread for each site, make a post request to another site, if the response.code that comes back is a 500, wait 15 seconds and try again. Try only 5 times, upon the fifth failure - send an email
I am trying to do this in such a way that the code is easily testable with output having to validate that a thread is created. In other words, my tests only care about the response coming back from the request, I have integration tests that test the actual "wait 15 seconds ... " part.
What I have so far is:
module BlackBird
module PublishToSites
module User
def self.publish_user(params)
if Site.all != nil
threads = []
Site.all.each do |s|
threads << Thread.new do
publish(s, params)
end
end
threads.each { |t| t.join }
else
# Send Email
# - Use No Sites Template
end
end
private
def self.publish(site, params)
response = Typhoeus.post(
s.site_api_url + 'users',
:body => params.to_json,
:headers => {
"Authorization" => "Token #{s.site_api_key}",
'Content-Type' => 'application/json'
}
)
return deal_with_response(response)
end
def self.deal_with_response(response)
if response.code == 200
elsif response.code == 500
# wait and try again after 15 seconds for a total of 5 times.
# should we fail on the 5th try, use the email template: Recieved 500.
end
end
end
end
Because this is running on the JVM I will have no issue with multithreading and things should generally run faster then the MRI, speed is of an essence here.
So How do I, once I reach the response.code == 500 section, actually say, wait 15 seconds, try again for a total of 5 times?
since you do not have this code structured and the Thread.new is not just an implementation detail you will need to pass a counter and the "full" state around to the place where you want to "sleep and retry" e.g. :
def self.publish(site, params, try = 0)
response = Typhoeus.post(
site.site_api_url + 'users',
:body => params.to_json,
:headers => {
"Authorization" => "Token #{site.site_api_key}",
'Content-Type' => 'application/json'
}
)
return deal_with_response(response, site, params, try)
end
def self.deal_with_response(response, site, params, try = 0)
if response.code == 200
elsif response.code == 500
# wait and try again after 15 seconds for a total of 5 times.
# should we fail on the 5th try, use the email template: Recieved 500.
if ( try += 1 ) < 5
sleep(15)
publish(site, params, try)
else
send_email "received response 500 for 5 times"
end
end
end
be aware since you're joining on the created threads that the requests will wait potentially ~ 5x15 seconds before returning a response ... when there are 500 failures!

Get error message out of Sidekiq job

I want to get exception error message out of the sidekiq job. when I set back_trace option to true it retries my job but I want to exit from job when error raises and get error message.
if I find that process ended successful or fail is enough.
def perform(text)
begin
fail StandardError, 'Error!'
rescue
fail 'EEE' # I want to get this error when call job
end
end
# call
NormalJob.perform_async('test')
# I want to get error here after call
If I were you I would try gem sidekiq-status. It has several options, which can be helpful in such situations:
You can retrieve status of your worker:
job_id = MyJob.perform_async(*args)
# :queued, :working, :complete or :failed , nil after expiry (30 minutes)
status = Sidekiq::Status::status(job_id)
Sidekiq::Status::queued? job_id
Sidekiq::Status::working? job_id
Sidekiq::Status::complete? job_id
Sidekiq::Status::failed? job_id
Also you have options for Tracking progress, saving and retrieveing data associated with job
class MyJob
include Sidekiq::Worker
include Sidekiq::Status::Worker # Important!
def perform(*args)
# your code goes here
# the common idiom to track progress of your task
total 100 # by default
at 5, "Almost done"
# a way to associate data with your job
store vino: 'veritas'
# a way of retrieving said data
# remember that retrieved data is always is String|nil
vino = retrieve :vino
end
end
job_id = MyJob.perform_async(*args)
data = Sidekiq::Status::get_all job_id
data # => {status: 'complete', update_time: 1360006573, vino: 'veritas'}
Sidekiq::Status::get job_id, :vino #=> 'veritas'
Sidekiq::Status::at job_id #=> 5
Sidekiq::Status::total job_id #=> 100
Sidekiq::Status::message job_id #=> "Almost done"
Sidekiq::Status::pct_complete job_id #=> 5
Another option is to use sidekiq batches status
This is what batches allow you to do!
batch = Sidekiq::Batch.new
batch.description = "Batch description (this is optional)"
batch.notify(:email, :to => 'me#example.org')
batch.jobs do
rows.each { |row| RowWorker.perform_async(row) }
end
puts "Just started Batch #{batch.bid}"
b = Sidekiq::Batch.new(bid) # bid is a method on Sidekiq::Worker that gives access to the Batch ID associated to the job.
b.jobs do
SomeWorker.perform_async(1)
sleep 1
# Uh oh, Sidekiq has finished all outstanding batch jobs
# and fires the complete message!
SomeWorker.perform_async(2)
end
status = Sidekiq::Batch::Status.new(bid)
status.total # jobs in the batch => 98
status.failures # failed jobs so far => 5
status.pending # jobs which have not succeeded yet => 17
status.created_at # => 2012-09-04 21:15:05 -0700
status.complete? # if all jobs have executed at least once => false
status.join # blocks until the batch is considered complete, note that some jobs might have failed
status.failure_info # an array of failed jobs
status.data # a hash of data about the batch which can easily be converted to JSON for javascript usage
It can be used out of the box

Rails backgroundRB plugin need to schedule it and queue to database for persistancy

I'm trying to do the following:
Run a Worker and a method within it every 15 minutes
Have a log of the job last runtime, in the database table
bdrd_job_queue.
What I've done:
I have a schedule every 15 minutes in my backgroundRB.yml file
The method call has a persistent_job.finish! call, but it's not working,
because the persistent_job object is nil.
How can I ensure it's logged in the DB, but still automatically
scheduled from backgroundRB.yml?
I was finally able to do it.
The workaround is to schedule a task that will queue it to the database, scheduled to run right away.
In your worker ...
class NotificationWorker < BackgrounDRb::MetaWorker
set_worker_name :notification_worker
def create(args = nil)
end
def queue_notify_changes(args = nil)
BdrbJobQueue.insert_job(:worker_name => 'notification_worker',
:worker_method => 'notify_new_changes_DAEMON',
:args => 'hello_world',
:scheduled_at => Time.now.utc,
:job_key => 'email_changes_notification_task')
end
def notify_new_changes_DAEMON
#Do Incredibly cool stuff here
end
In the config file backgroundrb.yml
---
:backgroundrb:
:ip: 0.0.0.0
:port: 11006
:environment: production
:log: foreground
:debug_log: true
:persistent_disabled: false
:persistent_delay: 10
:schedules:
:notification_worker:
:queue_notify_changes:
:trigger_args: 0 0 0 * * *

Resources