-
Notifications
You must be signed in to change notification settings - Fork 21.6k
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
Introduce custom serializers to ActiveJob arguments #30941
Introduce custom serializers to ActiveJob arguments #30941
Conversation
Thanks for the pull request, and welcome! The Rails team is excited to review your changes, and you should hear from @kamipo (or someone else) soon. If any changes to this PR are deemed necessary, please add them as extra commits. This ensures that the reviewer can see what has changed since they last reviewed the code. Due to the way GitHub handles out-of-date commits, this should also make it reasonably obvious what issues have or haven't been addressed. Large or tricky changes may require several passes of review and changes. This repository is being automatically checked for code quality issues using Code Climate. You can see results for this analysis in the PR status below. Newly introduced issues should be fixed before a Pull Request is considered ready to review. Please see the contribution instructions for more information. |
It is by design that we only serialize a small set of object. Im fine to allowing to define custom serializers, but this PR is adding more default serializers that we had before. Could you keep only the current types we have? |
@rafaelfranca would it be OK if we keep the serializers but remove them from the default list? |
@mpapis It will be confusing I think. What if we create a separate gem with additional serializers? |
@rafaelfranca you can check it |
ping @rafaelfranca @matthewd |
@EPecherkin can you describe a good use case when a Rails app would use a custom serializer? |
@kirs In our rails app we had a lot of boilerplate code to serialize parameters to basic types and then to deserialize them in the job, at one time we changed the types and this lead to more complicated code and even introduced bugs. With automated serialization this would be a lot less painful, not only it would prevent bugs but also it would make the code better. One of the classes was TimeWithZone, we had special code to serialize it and deserialize it around every job that was using it, with this serializers we define it once and it's done automatically from that point on. |
ping @rafaelfranca @matthewd @kirs |
activejob/lib/active_job/base.rb
Outdated
@@ -1,6 +1,7 @@ | |||
# frozen_string_literal: true | |||
|
|||
require "active_job/core" | |||
require "active_job/serializers" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can remove this require from here since we have autoload in place.
end | ||
|
||
# :nodoc: | ||
SERIALIZERS = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why instead of defining a private API constant we just don't use the add_serializers
method here?
class << self | ||
def serialize?(argument) | ||
argument.is_a?(klass) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should implement klass
, deserialize?
, serialize
and deserialize
and raise a NotImplementedError
|
||
def keys | ||
[key] | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should implement key
and raise NotImplementedError
.
|
||
module ActiveJob | ||
module Serializers | ||
class BaseSerializer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add documentation for this class and here also put the example you put in the guides
|
||
module ActiveJob | ||
module Serializers | ||
class ObjectSerializer < BaseSerializer |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing documentation for this class too
I like the idea of supporting custom serializers -- I think field use has confirmed that while there are advantages to preferring basic/universal types, it can be a pain to manually transform values on their way in & out. I don't think our current custom-hash-key-per-serializer model scales very well... it was fine when there was only one, and the two others enhance something that is still fundamentally a hash... but I think we've reached the end of its useful life. For a full-on registered serializer setup, I think we'd be better off defining a single new reserved key, probably named something like Beyond avoiding occupying an ever-increasing [albeit obscure] part of the possible hash key space, it also means we don't need to try every deserializer in turn: we know exactly which one can handle the value. We should probably retain the existing handling for the current reserved keys, for compatibility across upgrades and with any 3rd party / non-ruby code that's already learned how to handle them specially. Overall I think I'm suggesting that we keep the current As for adding new serializers by default, I think there are some that are worthwhile: symbol and duration as you previously had, and also Date, Time, DateTime, TimeWithZone. |
@matthewd is this what you had in mind? master...rafaelfranca:toptal-introduce-custom-serializers-to-activejob-arguments |
@rafaelfranca that looks great! I'm still not sure about the |
Yeah, good point. I'll revert the changes to keep the old behavior as the case statement and only when the value is a Hash I'll use the new behavior. |
Updated the PR with the new code. I was going to remove the |
😍
We could use a search over the to-be-serialized object's ancestors instead of a search over the serializers... I'm not sure whether that would be better. 🤷🏻♂️ I note your last change has restored the ability to deserialize a hash that has no special keys, which had [by my reading?] gone away inside HashSerializer. If I'm right about that, is it worth adding a test for that case? |
Yeah, I feel it would be worst if the ancestor chain is big and harder to optimize. Searching in the serializers we can change the order of the array and get the most used first. |
I just added the tests |
module Serializers | ||
class TimeSerializer < ObjectSerializer # :nodoc: | ||
def serialize(time) | ||
super("value" => time.to_s) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
time.iso8601
here and Time.iso8601(hash["value"])
to deserialize?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, it make sense to use a iso format.
Right now it is only possible to define serializers globally so we don't need to use a class attribute in the job class.
Now custom serialziers can register itself in the serialized hash using the "_aj_serialized" key that constains the serializer name. This way we can avoid poluting the hash with many reserved keys.
We can speed up things for the supported types by keeping the code in the way it was. We can also avoid to loop trough all serializers in the deserialization by trying to access the class already in the Hash. We could also speed up the custom serialization if we define the class that is going to be serialized when registering the serializers, but that will remove the possibility of defining a serialzer for a superclass and have the subclass serialized using it.
This will make easier to be backwards compatible when changing the serialization implementation.
Improve ActiveJob custom argument serializers #30941
Summary
The way to serialize arguments for ActiveJob was completely reworked.
This PR brings an ability to define custom serializers for almost any object. A developer needs just to implement a simple interface.
And add this serializer to a list:
Testing
activejob
folderirb -r ./lib/active_job.rb -r ./test.rb
in terminaldeserialize(serialize(ARGUMENT)) == ARGUMENT
.ARGUMENT
contains all possible objects for serialization. But you can experiment as you wish