deliver broadcast messages asynchronously#41
deliver broadcast messages asynchronously#41prog-supdex wants to merge 1 commit intoanycable:masterfrom
Conversation
d686343 to
f08db8e
Compare
graphql-anycable.gemspec
Outdated
| spec.add_dependency "anyway_config", ">= 1.3", "< 3" | ||
| spec.add_dependency "graphql", ">= 1.11", "< 3" | ||
| spec.add_dependency "redis", ">= 4.2.0" | ||
| spec.add_dependency "activejob", ">= 5.2.0" |
There was a problem hiding this comment.
It shouldn't be a dependency; the gem is Rails-free, we only provide ActiveJob integration if ActiveJob is available in the runtime.
There was a problem hiding this comment.
I added it to spec.add_development_dependency "activejob", ">= 6.0.0"
because otherwise, I got an error during running specs
lib/graphql-anycable.rb
Outdated
| require_relative "graphql/anycable/cleaner" | ||
| require_relative "graphql/anycable/config" | ||
| require_relative "graphql/anycable/railtie" if defined?(Rails) | ||
| require_relative "graphql/adapters/base_job" |
There was a problem hiding this comment.
Move into graphql/anycable/railtie and load only if ActiveJob is present (using ActiveSupport.on_load(:active_job) { ... } hook).
There was a problem hiding this comment.
yeah, I done it, but I had an error in this case
initializer 'graphql_anycable.load_trigger_job' do
ActiveSupport.on_load(:active_job) do
require "graphql/adapters/trigger_job"
end
end
in that case, my application (rails 6, with configurated active_job) does not load it, because my application does not call any ActiveJob jobs in the code
I checked and saw, that ActiveSupport.on_load(:active_job) loads only when my rails application calls any jobs
but, maybe I misunderstood something.
Currently, I just commented ActiveSupport.on_load(:active_job) and I think, if we are in Railtie, we can require TriggerJob in initialize block
There was a problem hiding this comment.
Got it.
I'm looking at how ActionMailer or ActiveStorage does this, and it seems that we can just add require "active_job/railtie" and load our job class after that.
There was a problem hiding this comment.
thanks for that!
The trouble was with my test application
lib/graphql/adapters/base_job.rb
Outdated
| class BaseJob < ActiveJob::Base | ||
| DEFAULT_QUEUE_NAME = :default | ||
|
|
||
| queue_as { GraphQL::AnyCable.config.async_broadcasting["queue"] || DEFAULT_QUEUE_NAME } |
There was a problem hiding this comment.
The default name must be define in the configuration, no need to use a fallback here:
| queue_as { GraphQL::AnyCable.config.async_broadcasting["queue"] || DEFAULT_QUEUE_NAME } | |
| queue_as do | |
| AnyCable.config.active_job_queue_name | |
| end |
There was a problem hiding this comment.
Or, we go with adapter instances, we can specify the queue right when calling #perform_later:
TriggerJob.set(queue: queue).perform_later(...)
lib/graphql/adapters/base_job.rb
Outdated
| private | ||
|
|
||
| def schema_parse(serialized_payload) | ||
| payload = JSON.parse(serialized_payload) |
There was a problem hiding this comment.
Maybe, we can use symbolize_names here and avoid #transform_keys?
| payload = JSON.parse(serialized_payload) | |
| payload = JSON.parse(serialized_payload, symbolize_names: true) |
There was a problem hiding this comment.
Of course, yeah. We can just put symbolized hash to ActiveJob arguments, and it will work without JSON.parse
lib/graphql/anycable/config.rb
Outdated
| attr_config use_async_broadcasting: true | ||
| attr_config async_broadcasting: { queue: "broadcasting", class: "GraphQL::Adapters::BaseJob" } |
There was a problem hiding this comment.
This is not how adapter pattern is implemented. The API must be as follows:
GraphQL::AnyCable.delivery_method = :inline
GraphQL::AnyCable.delivery_method = :active_job
# or with options
GraphQL::AnyCable.delivery_method = :active_job, queue: "custom_queue", job_class: "MyJob"And then, in the #trigger method we simply delegate the execution to the configured adapter instance. Smth like:
def trigger(...)
AnyCable.delivery_adapter.trigger(...)
endThere was a problem hiding this comment.
I did it, but without "="
It works like that
GraphQL::AnyCable.delivery_method("active_job", queue: "custom_queue", job_class: "MyJob")
But, if we need like setter, we will able to call it only like that (adds { and } for options)
GraphQL::AnyCable.delivery_method = :active_job, {queue: "custom_queue", job_class: "MyJob"}
Because the setter, as I know, cannot accept keyword arguments
GraphQL::AnyCable.delivery_method = "inline", queue: "queue", job_class: "Job"
SyntaxError ((irb):7: syntax error, unexpected tLABEL)
...ble.delivery_method = 1, queue: 123, job_class: "Abc"
... ^~~~~~
There was a problem hiding this comment.
if we need like setter, we will able to call it only like that
Yes, and that's how it should be done. That's a common pattern used by Rails and many libraries, We shouldn't reinvent it.
So, please, implement it using the original proposed interface.
P.S. See, for example: https://github.com/palkan/active_delivery/blob/master/lib/abstract_notifier/async_adapters.rb
There was a problem hiding this comment.
mm, yea, I earlier said, why I did not do that
I fixed it, but it will work only with
GraphQL::AnyCable.delivery_method = :active_job, {queue: "custom_queue", job_class: "MyJob"}
so, we need to wrap by { and } last arguments
| end | ||
|
|
||
| executor_class_job.perform_later( | ||
| serialized_arguments, |
There was a problem hiding this comment.
We should not serialize arguments; it's the responsibility of Active Job; we may assume that arguments are GlobalID identifiable, so we can pass them as is.
There was a problem hiding this comment.
yea, currently I do not serialize them, but I still collected them (arguments)
|
|
||
| return Adapters::BaseJob unless custom_class | ||
|
|
||
| Object.const_get(config.async_broadcasting["class"]) |
There was a problem hiding this comment.
The job class is inferred in Rails context where we can use #safe_constantize
There was a problem hiding this comment.
Thanks!
But for job_class, I put #constantize instead of #safe_constantize, because I think that #constantize will be useful for cases when the user puts anything wrong to param job_class.
And constantize will raise an error and we will see it immediately
But, if I am wrong, I will fix it
| Object.const_get(config.async_broadcasting["class"]) | ||
| end | ||
|
|
||
| def perform(event, object) |
There was a problem hiding this comment.
It was pre-alfa version of the code, sorry :)
spec/adapters/base_job_spec.rb
Outdated
| require "active_job" | ||
|
|
||
| RSpec.describe GraphQL::Adapters::BaseJob, type: :job do | ||
| ActiveJob::Base.queue_adapter = :inline |
There was a problem hiding this comment.
Never define configuration outside of before and after hooks, and always restore the value after each test. Otherwise it's easier to introduce unpredictable global state and flakiness.
There was a problem hiding this comment.
Thank you for the advice!
79c4a14 to
1dee021
Compare
| @executor_method = executor_object.class::EXECUTOR_METHOD_NAME | ||
| end | ||
|
|
||
| def trigger(...) |
There was a problem hiding this comment.
I use here "...", but It requires "ruby-2.7".
I checked ci-cd, and I saw minimal ruby versions - 2.7, but if we look at the documentation (in anycable-rails, because it is a dependency), I see the requirement for ruby-version 2.6
There was a problem hiding this comment.
@palkan please tell me, do we need to consider ruby 2.6 and avoid using "..." ?
If yes, I will fix it
lib/graphql-anycable.rb
Outdated
| require_relative "graphql/anycable/cleaner" | ||
| require_relative "graphql/anycable/config" | ||
| require_relative "graphql/anycable/railtie" if defined?(Rails) | ||
| require_relative "graphql/adapters/base_job" |
There was a problem hiding this comment.
Got it.
I'm looking at how ActionMailer or ActiveStorage does this, and it seems that we can just add require "active_job/railtie" and load our job class after that.
| @@ -3,31 +3,19 @@ | |||
| module GraphQL | |||
| module Adapters | |||
| class DeliveryAdapter | |||
There was a problem hiding this comment.
The goal of adapter patter is to provide extensibility. Moving all different implementations into a single file and calling it an "adapter" doesn't make it an adapter.
There was a problem hiding this comment.
Initially, I didn`t understand what was needed, but I now got it.
Now we can easily add any other adapter with his logic and add it in config.delivery_method
Thanks for the lesson
lib/graphql/anycable/config.rb
Outdated
| attr_config use_async_broadcasting: true | ||
| attr_config async_broadcasting: { queue: "broadcasting", class: "GraphQL::Adapters::BaseJob" } |
There was a problem hiding this comment.
if we need like setter, we will able to call it only like that
Yes, and that's how it should be done. That's a common pattern used by Rails and many libraries, We shouldn't reinvent it.
So, please, implement it using the original proposed interface.
P.S. See, for example: https://github.com/palkan/active_delivery/blob/master/lib/abstract_notifier/async_adapters.rb
b9a692a to
ebc7e13
Compare
|
@palkan hi! I sum up the changes Currently, we have two adapters, "Inline" and "ActiveJob" in two files The specific adapter is chosen by Also, we have a custom serializer AnyCableSubscriptionSerializer which helps do not think about an instance of class But for example here I used the arguments forwarding shorthand, which exists in ruby 2.7 (I see that ci-cd checks on ruby 2.7 and upper), but this gem has a dependency Maybe here I need not to use the arguments forwarding shorthand |
ebc7e13 to
70dfe1c
Compare
Deliver messages, which are called by method
trigger, asynchronouslyIt`s a not final PR