-
Notifications
You must be signed in to change notification settings - Fork 77
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
Support Tasks with custom parameters #413
Conversation
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.
I think we're missing a CSS class, the field is almost invisible:
Does it make sense to start the job and raise if parameters are invalid?
Potentially the validations would be quite costly, so I think it's ok to make them in before_perform
. In the mean time, if the validations are costly, it also means we do them at each interruption. Validations have a concept of context which we could use to decide which ones to run when.
I guess we could assume they're the same kind of validations that would happen in a regular controller flow, without any job involved, for now. If we realize there's a need for deeper more intensive validations, we could add them in the on_start
callback to run only once.
Looks great! I have a few comments but overall really neat.
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 is exciting! We were talking in https://github.com/Shopify/arrive-server/pull/17707 about how we needed maintenance_tasks
to support parameters to be able to convert some ShipIt tasks and now its here 👏
Looking at our internal usage, it seems like the most commonly used param types are:
The latter two are super common; so common, in fact, that it makes me think that people will just copy that validation regex over and over again. Would it make sense to have first-class support for array of string and array of numbers (maybe with a default, named validation)? I don't think it's a blocker for this PR though! |
@etiennebarrie what browser are you using? It renders with a dark outline for me in Chrome:
Good point, I overlooked the fact that these validations would run every time the job interrupts. But, this is potentially a benefit as you mentioned, since they might be evaluated in to a context that changes based on when the job first runs vs when it's being resumed. Let's leave it in |
@dirceu that's a great idea! The whole reason I went with Lemme see what I can whip up |
Firefox!
I feel like this is the least interesting choice. Either we're worried about the potential cost of validations and we do it once before the whole run, or we're not and we can do it at the controller level. "Validation" code can run inside
Yeah that would be cool, but either we introduce our own DSL (instead of being a simple Active Model), or we add a type to Active Record deals pretty well with invalid data: Post.where(id: "2,foo,4".split(",")).to_sql
# => "SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"id\" IN (2, 4)" One other thing I had missed is that this doesn't persist the parameters, so we can't pause/resume unless I'm missing something? |
Okay, might be a Bulma + Firefox thing in that case @etiennebarrie ? Gotcha, yeah I misinterpreted what you were getting at before re: validations needing to run in certain contexts. Let's just move it into the controller for now. In terms of types, IMO a nice way to do it would be to to add module MaintenanceTasks
module Parameters
class IntegerArrayType < ActiveModel::Type::Value
def cast(input)
return unless input.present?
validate_input(input)
input.split(",").map(&:to_i)
end
private
def validate_input(input)
unless /\A(\s?\d+(,\s?\d+\s?)*\s?)\z/.match?(input)
error_message = "IntegerArrayType expects expects a comma-delimited "\
"string of integers. Input received: #{input}"
raise TypeError.new(error_message)
end
end
end
end
end module Maintenance
class ParamsTask < MaintenanceTasks::Task
attribute :post_ids, MaintenanceTasks::Parameters::IntegerArrayType.new
validates :post_ids, presence: true
.... IMHO this is a much better experience than requiring every user to validate / sanitize input strings from the UI or write their own type.
LOL I completely overlooked this 😆 Should we persist params on the run as well? It might be a nice feature to be able to see that data historically in the UI as well (beyond just logging params, making them visible in the UI should increase confidence in terms of being able to track down what happened if someone ran a Task with faulty params) |
096367c
to
3c68d96
Compare
I've extracted playing around with And then made changes to this PR:
cc @etiennebarrie ready for some more feedback 🙇♀️ |
d1048c7
to
3701bf1
Compare
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.
Two main things:
- CLI support
- parameters vs arguments naming
But it's looking great! ✨
app/models/maintenance_tasks/run.rb
Outdated
|
||
attr_readonly :task_name | ||
|
||
serialize :backtrace | ||
serialize :parameters, Hash |
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.
I noticed it still gets saved as:
--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess
post_ids: '4,2'
Can we turn the arguments Hash into a proper Hash somehow?
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 not support a hash with indifferent access though, since this is how Rails represents hashes by default?
@etiennebarrie addressed most of your comments, there were a couple I'm not sure about, feel free to follow up 😄 Re: CLI support, I think it should be almost all the way there (probably just need to add the option + document it in the CLI?) , but I'd prefer to ship that separately and move forward with this. |
86967ed
to
c2cb0b0
Compare
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.
I'm still not a fan of persisting ruby/hash:ActiveSupport::HashWithIndifferentAccess
in the database, and I think we should support CLI before shipping a new gem version supporting tasks with parameters, but we can figure it out later.
We still sleep where we shouldn't though.
@etiennebarrie we could serialize it to JSON if you'd prefer? |
Maybe that would make more sense 🤔 |
Since we have parameters, should we still prevent running multiple tasks with different arguments? It probably requires a few changes because we've always assumed "one Task = zero or one Run", but conceptually, two runs with different arguments are similar to two different Tasks. I don't think it's reasonable to tackle this here, but just leaving this here. |
@etiennebarrie re: parallelism, I'd like for us to take the time to explore viable solutions for that, with one of them being allowing multiple instances of the same Task to be run concurrently (with args). It does change some fundamental assumptions within our system as you mentioned, so I think we want to commit to it as the single solution we're going with for concurrency, rather than building it in as a temporary workaround if we intend to build out concurrency fully. With batch support built in, I suspect a lot of the use cases pushing for concurrency support will actually just be able to use batches. |
@@ -38,6 +39,12 @@ def run | |||
|
|||
private | |||
|
|||
def task_arguments | |||
return {} unless params[:task_arguments].present? | |||
task_attributes = Task.named(params[:id]).attribute_names |
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.
Previously a wrong Task name would result in a ActiveRecord::RecordInvalid
raised from Runner and error caught in the action, now it will raise here and will result in a 500. We don't have tests for this because we didn't want controller tests and there's no way to test this with the UI (with a browser test, you'd need to see the UI and remove the task before clicking on Run). We could ignore that for now because like I mentioned in a previous review, parts of this logic should move to the Runner, with the CLI job needing similar handling, and fix it once we support arguments from the CLI.
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.
Hum, interesting. Let's leave it for now, I can remove it in the CLI PR if we choose to go that route and we feel it's redundant. Alternatively, we could keep the use of the Strong Parameters API here and just rescue Task::NotFoundError
and return {}
. I'm suspecting that we may just want to check the keys in validate_task_arguments
though, since we're already rescuing on Task::NotFoundError
there. But I'll defer to the CLI PR.
@etiennebarrie latest round of changes in 😁 Can you let me know:
|
35a4314
to
0d548e3
Compare
I'm going to 🚢 , if anything additional comes up we can fix forward (along with CLI support). |
Closes: #406, #325
This PR introduces support for custom Task parameters by leveraging
ActiveModel
attributes & validations.Task authors use
ActiveModel::Attributes
for their parameters, and can define their own validations to ensure any parameters that are passed from the UI conform to their expectations (ie.Maintenance::ParamsTask
uses a presence validator and a regex to ensure that post ids is a comma-delimited list of integers).Approach taken
MaintenanceTasks::Task
includes relevantActiveModel
modulesRunner#run
.MaintenanceTasks::Run
has a field forparameters
now, and we pass the parameters to the constructorTask
instance for therun
, and then validate whether the attributes are valid. If they aren't, we take the errors and add them to therun
, which gets propagated to the UI in the form of a flash alert.run
how has parameters persisted, and we've also set the parameters on theTask
so that any of the methods in theTask
(#process
,#collection
,#count
) now have access to these attribute🎩
1,2,3
in thePost ids
fieldPost ids
fieldPost ids
fieldTo Do
Let me know what you think!