Skip to content
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

Optimize JobWrapper queueing ActiveJobs #35

Merged
merged 6 commits into from
Jan 31, 2024

Conversation

caius
Copy link
Contributor

@caius caius commented Jan 11, 2024

This PR introduces a change to avoid calling ActiveJob::Base#deserialize during queueing the job whilst maintaining existing functionality.

The change is in JobWrapper, which previously only took in a hash of job_data and knew how to deserialize it into a job object so we can interrogate it for queue_name or max_attempts, etc. To avoid the roundtrip through serialization, we change JobWrapper#initialize to take in either the job object directly, or a hash as it did previously. When it's a hash (needed when running the job in the worker), it still deserializes it into a job object. When it's a job object just assign it to @job directly and continue.

We discovered this through having a second-order effect in #deserialize using it to trigger some logic as the job comes off the queue, but we were then seeing the logic run multiple times for one job execution. Tracked it back to the JobClass.perform_later calling #deserialize and being majorly confused why it was deserialising in the web process.

This will make enqueueing jobs faster too, because we don't need a roundtrip through #serialize, #deserialize as well as allocating less objects. In practice this is likely a small effect, but we get it for free avoiding the behaviour above.

Looking at all the built-in adapters in ActiveJob, as well as good_job and solid_queue, they all avoid calling job.serialize during the enqueuing, even those adapters that support invoking methods on the Job class instance like delayed does.

From one angle it's an optimization, from another angle it's a bugfix (:

From one angle it's an optimization, from another angle it's a bugfix (:

We don't need to call ActiveJob::Base.deserialize during queueing the job, because we already have an instance of the class we can send methods to for finding out the #max_attempts or #queue_name. Teach JobWrapper how to take a job instance in and serialize it into job_data in that case, whilst still supporting being loaded from the database as a hash and calling deserialize lazily (existing flow).

The potential bugfix is someone implementing a second-order effect in #deserialize like using it as a hook to setup ActiveSupport::CurrentAttributes for the job from persisted data. If we invoke #deserialize during enqueuing this can change CurrentAttributes values in the web request that's queueing the job.
lib/delayed/job_wrapper.rb Outdated Show resolved Hide resolved
lib/delayed/job_wrapper.rb Outdated Show resolved Hide resolved
Copy link
Member

@smudge smudge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution @caius! I made a couple of suggestions, and if you have a chance, feel free to bump the gem version & update the changelog! (If not I'll do that in a follow-up.)

Thanks! 🙌

Copy link
Contributor Author

@caius caius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@smudge thanks for the feedback, much nicer patch now. Think I've addressed everything if you could take another look please!

Comment on lines +21 to +33
## [0.5.2] - 2023-10-19
### Fixed

- Fixed regression for Rails 7.1.1 not executing jobs, due to lazy loading change in Rails.
The `JobWrapper` needs to be loaded before `ActiveJob::Base` is loaded, but this wasn't
happening in the worker processes. Fix by extracting `JobWrapper` into it's own file and
loading when `Delayed` is loaded earlier in the process.

## [0.5.1] - 2023-10-11
### Changed

- Explicitly test Rails 7.1 and Ruby 3.1, Ruby 3.2 to verify compatability. Previous
gems were compatible, but this release is now verified to be compatible.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@smudge I noticed the changelog wasn't up to date, so backfilled these entries based on the diffs/PR descriptions.

@caius
Copy link
Contributor Author

caius commented Jan 30, 2024

@smudge gentle ping? :)

@smudge smudge self-requested a review January 30, 2024 22:07
Copy link
Member

@smudge smudge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

domain LGTM && platform LGTM -- will merge and release shortly, thank you! 🙌

@smudge smudge merged commit f2e5edc into Betterment:main Jan 31, 2024
20 of 21 checks passed
@caius caius deleted the cd/serialize_lazily branch April 16, 2024 13:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants