How to fix Delayed Job deserialization errors after upgrading from Rails 4.2 to 5.1+? - ruby-on-rails

I am working on upgrading an app from Rails 4.2 to 5.2. I have am running into an issue were jobs that were created in 4.2 are raising errors when they are invoked under Rails 5.2.
Delayed::DeserializationError (Job failed to load: not delegated...
I have narrowed it down to a problem after moving from 5.0 to 5.1. In 5.0.7 there is no problem but there is in 5.1.0. I can reproduce on a simple test case (taken from job.handler) by doing YAML.load(yml) where yml:
object: !ruby/object:Account
raw_attributes:
id: '8469'
attributes: !ruby/object:ActiveRecord::AttributeSet
attributes: !ruby/object:ActiveRecord::LazyAttributeHash
types:
id: &4 !ruby/object:ActiveRecord::Type::Integer
precision:
scale:
limit: 8
range: !ruby/range
begin: -9223372036854775808
end: 9223372036854775808
excl: true
values:
id: '8469'
created_at: '2019-11-15 21:16:15.257401'
additional_types: {}
materialized: true
delegate_hash:
id: !ruby/object:ActiveRecord::Attribute::FromDatabase
name: id
value_before_type_cast: '8469'
type: *4
value: 8469
created_at: !ruby/object:ActiveRecord::Attribute::FromDatabase
name: created_at
value_before_type_cast: '2019-11-15 21:16:15.257401'
type: !ruby/object:ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter
subtype: !ruby/object:ActiveRecord::ConnectionAdapters::PostgreSQL::OID::DateTime
precision:
scale:
limit:
value: 2019-11-15 21:16:15.257401000 Z
new_record: false
active_record_yaml_version: 0
That gives the error ArgumentError (not delegated). I have found that removing the subtype under created_at makes the problem go away but not idea why. I have tried changing the subtype to something simple like an integer and get the same problem.
Anyone have thoughts on how to approach this? I am really surprised that I have not found any info on others running into the same problem.

Turned out it was bigger than just 5.0 being able to serialize but 5.1 couldn't. There are a bunch of methods and classes that have been moved or removed in Rails 5 so it is just difficult to deserialize. With some help by posting an issue in the delayed job issues (https://github.com/collectiveidea/delayed_job/issues/1111) I was able to come up with a solution. I ended up writing the following migration that deserializes and then serializes all delayed_jobs.
migrate_dj_to_rails5.rb
require 'ruby-progressbar'
class MigrateDjToRails5 < ActiveRecord::Migration[5.2]
include MigrationHelper
def up
execute create_backup('delayed_jobs')
migrator = DelayedJobMigrator.new(Delayed::Job.all)
migrator.migrate
end
def down
execute copy_field_from_backup('delayed_jobs', 'handler')
execute load_dropped_records_from_backup('delayed_jobs')
end
class DelayedJobMigrator
def initialize(jobs = Delayed::Job.all)
#exceptions = []
#corrected_job_ids = []
#deleted_job_ids = []
#jobs = jobs
#progress_bar = ProgressBar.create(total: jobs.count, format: "%t: |%w| %e")
end
def migrate_job(job)
begin
data = YAML.load_dj(job.handler)
rescue
begin
job.payload_object = TempToRuby.create.accept(Psych.parse(job.handler))
rescue => exception
if exception.message =~ /Couldn't find (.+) with 'id'=/
# job is no longer valid
#deleted_job_ids << job.id
job.delete
end
#exceptions << exception
return
end
job.save
#corrected_job_ids << job.id
end
end
def migrate
#jobs.find_each do |job|
migrate_job(job)
#progress_bar.increment
end
#corrected_job_ids.each do |id|
puts "Corrected job_id: #{id}"
end
#exceptions.each do |exception|
puts "Exceptions:: #{exception}"
end
puts "#{#corrected_job_ids.count} jobs corrected"
puts "#{#exceptions.count} exceptions encountered (should be same as jobs deleted)"
puts "#{#deleted_job_ids.count} jobs deleted"
end
class TempToRuby < Delayed::PsychExt::ToRuby
def visit_Psych_Nodes_Mapping(object)
if %r{^!ruby/object:ActiveRecord::AttributeSet}.match(object.tag.to_s)
{}
elsif %r{^!ruby/object:(.+)$}.match(object.tag.to_s)
klass = resolve_class(Regexp.last_match[1])
if klass < ActiveRecord::Base
payload = Hash[*object.children.map { |c| accept c }]
return super unless payload['raw_attributes']
id = payload['raw_attributes'][klass.primary_key]
klass.unscoped.find(id)
else
super
end
else
super
end
end
end
end
end
migration_helper.rb
# Helper module for commonly used migration related methods
module MigrationHelper
def create_backup(table_name, suffix='_copy')
table_name_copy = table_name + suffix
<<-SQL
DROP TABLE IF EXISTS #{table_name_copy};
CREATE TABLE #{table_name_copy} AS TABLE #{table_name};
SQL
end
def copy_field_from_backup(table_name, field, suffix='_copy')
table_name_copy = table_name + suffix
records_to_update = <<-SQL
UPDATE #{table_name}
SET #{field} = #{table_name_copy}.#{field}
FROM #{table_name_copy}
WHERE #{table_name_copy}.id = #{table_name}.id
SQL
end
def load_dropped_records_from_backup(table_name, suffix='_copy')
table_name_copy = table_name + suffix
<<-SQL
INSERT INTO #{table_name}
SELECT #{table_name_copy}.* FROM #{table_name_copy}
LEFT JOIN #{table_name} on #{table_name}.id = #{table_name_copy}.id
where #{table_name}.id is null
SQL
end
end

Related

Rails 5 find adult users

I've got pretty old App where I have to create rake task to find all users over 18 and update flags from adult: false to adult: true. I'm wondering what I should use in a rather old version of Rails (I have Rails 5 and Ruby 2.4 on board) to keep the highest performance?
What I have for now is a sidekiq worker with, I think, some syntax error:
class MinorsWorker
include Sidekiq::Worker
def perform
adults = User.where(adults: false).where('date_of_birth >= 18, ?', ((Time.zone.now - date_of_birth.to_time) / 1.year.seconds))
adults.update(adult: true)
end
end
But this code gives me an error:
NameError: undefined local variable or method `date_of_birth' for main:Object
you can do the following. This would update all the matched records in 1 update statement.
If you are concern with db IO, you can batch it.
# in user.rb
class User
scope :adult, -> { where('date_of_birth <= ?', 18.years.ago) }
end
# in your worker file
class MinorsWorker
include Sidekiq::Worker
def perform
update_all
# for update in batches, use #update_all_in_batches
end
private
def update_all
User.adult.where(adult: false).update_all(adult: true)
end
def update_all_in_batches
User.adult.where(adult: false).in_batches(each: 1000) do |users|
users.update_all(adult: true)
sleep 2
end
end
end

undefined method `set' for nil:NilClass in Rails even though similar code works in irb

The following code works fine in IRB (Interactive Ruby Shell):
require 'prometheus/client'
prometheus = Prometheus::Client.registry
begin
#requests = prometheus.gauge(:demo, 'Random number selected for this users turn.')
rescue Prometheus::Client::Registry::AlreadyRegisteredError => e
end
#requests.set({name: "test"}, 123)
test = #requests.get name: "test"
puts 'output: ' + test.to_s
2.4.0 :018 > load 'test.rb'
output: 123.0
=> true
2.4.0 :019 >
However, when I put the same code into my Ruby on Rails controller, the second time the user uses the application, the following error is returned:
undefined method `set' for nil:NilClass
Can someone tell me when I'm doing wrong? Thank you.
require 'prometheus/client'
class RandomnumbersController < ApplicationController
def index
#randomnumbers = Randomnumber.order('number DESC').limit(8)
#counter = 0
end
def show
#randomnumber = Randomnumber.find(params[:id])
end
def new
end
def create
#randomnumber = Randomnumber.new(randomnumber_params)
prometheus = Prometheus::Client.registry
begin
#requests = prometheus.gauge(:demo, 'Random number selected for this users turn.')
rescue Prometheus::Client::Registry::AlreadyRegisteredError => e
end
#requests.set({name: "test"}, 123)
test = #requests.get name: "test"
#randomnumber.save
redirect_to #randomnumber
end
private
def randomnumber_params
params.require(:randomnumber).permit(:name, :number)
end
end
Because there is no #requests for :demo argument.
When ORM cannot find any info in db it returns nil (NilClass)
and You're trying to do:
#requests.set({name: "test"}, 123)
it's interpreted like:
nil.set({name: "test"}, 123)
why it's causes this issue in second time?
cuz Your code changes #requests name attribute to be test and seems like :demo is not test or maybe in another part of Your app You're replacing/deleting data in database that makes: #requests = prometheus.gauge(:demo, 'Random number selected for this users turn.') to return nil
Solution:
in code level add this fixes to avoid such unpredictable situations (check for nil) :
unless #requests.nil?
#requests.set({name: "test"}, 123)
test = #requests.get name: "test"
end

How can I avoid deadlocks on my database when using ActiveJob in Rails?

I haven't had a lot of experience with deadlocking issues in the past, but the more I try to work with ActiveJob and concurrently processing those jobs, I'm running into this problem. An example of one Job that is creating it is shown below. The way it operates is I start ImportGameParticipationsJob and it queues up a bunch of CreateOrUpdateGameParticipationJobs.
When attempting to prevent my SQL Server from alerting me to a ton of deadlock errors, where is the cause likely happening below? Can I get a deadlock from simply selecting records to populate an object? Or can it really only happen when I'm attempting to save/update the record within my process_records method below when saving?
ImportGameParticipationsJob
class ImportGameParticipationsJob < ActiveJob::Base
queue_as :default
def perform(*args)
import_participations(args.first.presence)
end
def import_participations(*args)
games = Game.where(season: 2016)
games.each do |extract_record|
CreateOrUpdateGameParticipationJob.perform_later(extract_record.game_key)
end
end
end
CreateOrUpdateGameParticipationJob
class CreateOrUpdateGameParticipationJob < ActiveJob::Base
queue_as :import_queue
def perform(*args)
if args.first.present?
game_key = args.first
# get all particpations for a given game
game_participations = GameRoster.where(game_key: game_key)
process_records(game_participations)
end
end
def process_records(participations)
# Loop through participations and build record for saving...
participations.each do |participation|
if participation.try(:player_id)
record = create_or_find(participation)
record = update_record(record, participation)
end
begin
if record.valid?
record.save
else
end
rescue Exception => e
end
end
end
def create_or_find(participation)
participation_record = GameParticipation.where(
game_id: participation.game.try(:id),
player_id: participation.player.try(:id))
.first_or_initialize do |record|
record.game = Game.find_by(game_key: participation.game_key)
record.player = Player.find_by(id: participation.player_id)
record.club = Club.find_by(club_id: participation.club_id)
record.status = parse_status(participation.player_status)
end
return participation_record
end
def update_record(record, record)
old_status = record.status
new_status = parse_status(record.player_status)
if old_status != new_status
record.new_status = record.player_status
record.comment = "status was updated via participations import job"
end
return record
end
end
They recently updated and added an additional option you can set that should help with the deadlocking. I had the same issue and was on 4.1, moving to 4.1.1 fixed this issue for me.
https://github.com/collectiveidea/delayed_job_active_record
https://rubygems.org/gems/delayed_job_active_record
Problems locking jobs
You can try using the legacy locking code. It is usually slower but works better for certain people.
Delayed::Backend::ActiveRecord.configuration.reserve_sql_strategy = :default_sql

Transaction unable to Rollback beyond certain Model

I have to process a very long form with multiple models.
def registerCandidate
action_redirect = ""
id = ""
ActiveRecord::Base.transaction do
begin
#entity = Entity.new( name: params[:entity][:name], description: params[:entity][:description], logo: params[:entity][:logo])
#access = Access.new( username: params[:access][:username], password: params[:access][:password], remember_me: params[:access][:rememberme], password_confirmation: params[:access][:password_confirmation])
#access.entity = #entity
#access.save!
#biodata = Biodatum.new(
date_of_birth: params[:biodatum][:birthday],
height: params[:biodatum][:height],
family_members: params[:biodatum][:family_members],
gender: params[:biodatum][:gender],
complexion: params[:biodatum][:complexion],
marital_status: params[:biodatum][:marital_status],
blood_type: params[:biodatum][:blood_type],
religion: params[:biodatum][:religion],
education: params[:biodatum][:education],
career_experience: params[:biodatum][:career_experience],
notable_accomplishments: params[:biodatum][:notable_accomplishments],
emergency_contact: params[:biodatum][:emergency_contact],
languages_spoken: params[:biodatum][:languages_spoken]
)
#biodata.entity = #entity
#biodata.save!
#employee = Employee.new()
#employee.entity = #entity
#employee.save!
action_redirect = "success_candidate_registration"
id = #access.id
#action_redirect = "success_candidate_registration?id=" + #access.id
#Error Processing
rescue StandardError => e
flash[:collective_errors] = "An error of type #{e.class} happened, message is #{e.message}"
action_redirect = "candidate_registration"
end
end
redirect_to action: action_redirect, access_id: id
end
If I raise any error beyond #access.save! it does the entire transaction without rolling back. How do you modify in order for any error related to all models rollback everything?
Since you are rescuing StandardError - which most errors derive from you could very well be swallowing errors which could cause a rollback.
This is commonly known as Pokemon exception handling (Gotta catch em' all) and is an anti-pattern.
Instead listen for more specific errors such as ActiveRecord::RecordInvalid - and raise a raise ActiveRecord::Rollback on error to cause a rollback.
If you haven't already read the Active Record Transactions docs, there is some very good information there about what will cause a rollback.
Also if you want to be a good Ruby citizen and follow the principle of least surprise use snakecase (register_candidate) not camelCase (registerCandidate) when naming methods.
Added
Instead of copying every value from params to your models you should use strong parameters and pass the model a hash.
This reduces code duplication between controller actions and keeps your controllers nice and skinny.
class Biodatum < ActiveRecord::Base
alias_attribute :date_of_birth, :birthday # allows you to take the :birthday param
end
# in your controller:
def register_candidate
# ...
#biodata = Biodatum.new(biodatum_params)
# ...
end
private
def biodatum_params
params.require(:biodatum).allow(
:birthday, :height, :family_members :family_members
) # #todo whitelist the rest of the parameters.
end

Manually set updated_at in Rails

I'm migrating my old blog posts into my new Rails blog, and I want their updated_at attribute to match the corresponding value on my old blog (not the date they were migrated into my new Rails blog).
How can I do this? When I set updated_at manually it gets overridden by the before_save callback.
Note: This question is only valid for Rails < 3.2.11. Newer versions of Rails allow you to manually set timestamps without them being overwritten.
If it's a one time thing you can turn record_timestamps on or off.
ActiveRecord::Base.record_timestamps = false
#set timestamps manually
ActiveRecord::Base.record_timestamps = true
When I ran into this issue with my app, I searched around for a bit and this seemed like it made the most sense to me. It's an initializer that I can call where I need to:
module ActiveRecord
class Base
def update_record_without_timestamping
class << self
def record_timestamps; false; end
end
save!
class << self
def record_timestamps; super ; end
end
end
end
end
As of recent versions of Rails (3.2.11 as per iGELs comment) you can set the updated_at property in code and the change will be honoured when saving.
I assume rails is keeping track of 'dirty' properties that have been manually changed and not overwriting on save.
> note = Note.last
Note Load (1.4ms) SELECT "notes".* FROM "notes" ORDER BY "notes"."id" DESC LIMIT 1
=> #<Note id: 39, content: "A wee note", created_at: "2015-06-09 11:06:01", updated_at: "2015-06-09 11:06:01">
> note.updated_at = 2.years.ago
=> Sun, 07 Jul 2013 21:20:47 UTC +00:00
> note.save
(0.4ms) BEGIN
(0.8ms) UPDATE "notes" SET "updated_at" = '2013-07-07 21:20:47.972990' WHERE "notes"."id" = 39
(0.8ms) COMMIT
=> true
> note
=> #<Note id: 39, content: "A wee note", created_at: "2015-06-09 11:06:01", updated_at: "2013-07-07 21:20:47">
So short answer, workarounds are not needed any longer in recent versions of rails.
I see two ways to accomplish this easily:
touch (Rails >=5)
In Rails 5 you can use the touch method and give a named parameter time like described in the documentation of touch
foo.touch(time: old_timestamp)
update_column (Rails >=4)
If you want it in Rails 4 and lower or want to avoid all callbacks you could use one of the update_column or update_columns methods which bypass all safe or touch callbacks and validations
foo.update_column(updated_at, old_timestamp)
or
foo.update_columns(updated_at: old_timestamp)
I took Andy's answer and modified it to accept blocks:
module ActiveRecord
class Base
def without_timestamping
class << self
def record_timestamps; false; end
end
yield
class << self
remove_method :record_timestamps
end
end
end
end
This is riffing off of Andy Gaskell's answer:
class ActiveRecord::Base
class_inheritable_writer :record_timestamps
def do_without_changing_timestamps
self.class.record_timestamps = false
yield
ensure
self.class.record_timestamps = true
end
end
The solution is to temporarily set ActiveRecord::Base.record_timestamps to false:
ActiveRecord::Base.record_timestamps = false
# Make whatever changes you want to the timestamps here
ActiveRecord::Base.record_timestamps = true
If you want a somewhat more robust solution, you may want to try something like what mrm suggested:
module ActiveRecord
class Base
def self.without_timestamping
timestamping = self.record_timestamps
begin
self.record_timestamps = false
yield
ensure
self.record_timestamps = timestamping
end
end
end
end
Then you can easily make changes to models without their timestamps being automatically updated:
ActiveRecord::Base.without_timestamping do
foo = Foo.first
bar = Bar.first
foo.updated_at = 1.month.ago
bar.updated_at = foo.updated_at + 1.week
foo.save!
bar.save!
end
Or, if you only want to update records from a specific class without timestamping:
module ActiveRecord
class Base
# Don't delete Rail's ActiveRecord::Base#inherited method
self.singleton_class.send(:alias_method, :__inherited__, :inherited)
def self.inherited(subclass)
__inherited__
# Adding class methods to `subclass`
class << subclass
def without_timestamping
# Temporarily override record_timestamps for this class
class << self
def record_timestamps; false; end
end
yield
ensure
class << self
remove_method :record_timestamps
end
end
end
end
end
end
E.g:
Foo.without_timestamping do
foo = Foo.first
bar = Bar.new(foo: foo)
foo.updated_at = 1.month.ago
foo.save! # Timestamps not automatically updated
bar.save! # Timestamps updated normally
end
Or you could use an approach similar to what Venkat D. suggested, which works on a per-instance basis:
module ActiveRecord
class Base
def without_timestamping
class << self
def record_timestamps; false; end
end
yield
ensure
class << self
remove_method :record_timestamps
end
end
end
end
E.g:
foo = Foo.first
foo.without_timestamping do
foo2 = Foo.new(parent: foo)
foo.updated_at = 1.month.ago
foo.save! # Timestamps not automatically updated
foo2.save! # Timestamps updated normally
end

Resources