Set `importing` on objects through global context
<!--IssueSummary start--> <details> <summary> Everyone can contribute. [Help move this issue forward](https://handbook.gitlab.com/handbook/marketing/developer-relations/contributor-success/community-contributors-workflows/#contributor-links) while earning points, leveling up and collecting rewards. </summary> - [Close this issue](https://contributors.gitlab.com/manage-issue?action=close&projectId=278964&issueIid=434114) </details> <!--IssueSummary end--> ### About There are often validations and callbacks on models that should be switched off during an import. See [example validation](https://gitlab.com/gitlab-org/gitlab/-/blob/de69f32e5f23d683d10eb2ef4dab7b321551a3ec/app/models/note.rb#L111) and [example callback](https://gitlab.com/gitlab-org/gitlab/-/blob/de69f32e5f23d683d10eb2ef4dab7b321551a3ec/app/models/note.rb#L200). We do this: - To optimise away performance problems in callbacks (example: https://gitlab.com/gitlab-org/gitlab/-/issues/336788). - Because order of data created might make an object invalid. ### Problem To disable callbacks on an object we create during an import, we must always remember to set `importing: true` on the object before saving, if that object's class includes the `Importable` module. We often forget to set `importing: true` on all objects before we save them. So the current method is error-prone and brittle. Some problems when we forget to set this property: - Callbacks that should be disabled for performance reasons trigger. For example, this [callback on `Note`](https://gitlab.com/gitlab-org/gitlab/-/blob/3473ab1f2a2fa774e4d29fcf3a1d46e1501ed7e2/lib/gitlab/markdown_cache/active_record/extension.rb#L15) should be switched off during an import to protect GitLab's database https://gitlab.com/gitlab-org/gitlab/-/issues/336788, and [this touch](https://gitlab.com/gitlab-org/gitlab/-/blob/de69f32e5f23d683d10eb2ef4dab7b321551a3ec/app/models/note.rb#L200) among others. - Object might fail to import due to validations. This seems to be extensive, examples spotted in just `github_import/importer/events` are: - [`ResourceMilestoneEvent.create!`](https://gitlab.com/gitlab-org/gitlab/-/blob/c7e4ef1b65fc232f89f5b399a45d7b1e5942db25/lib/gitlab/github_import/importer/events/changed_milestone.rb#L20-28) won't [disable this callback](https://gitlab.com/gitlab-org/gitlab/-/blob/1d888461ae7a465b1c83c4d7aa6c75dbd6c43222/app/models/resource_timebox_event.rb#L9) - A [`Note.create!`](https://gitlab.com/gitlab-org/gitlab/-/blob/3709ada936d6c075dbec5e592a4e5d787ed3fe60/lib/gitlab/github_import/importer/events/changed_assignee.rb#L20) won't disable many callbacks switched off for performance reasons. - Another [`Note.create!`](https://gitlab.com/gitlab-org/gitlab/-/blob/f1fa496c959245dacfe0f797f93392912291cdf9/lib/gitlab/github_import/importer/events/changed_reviewer.rb#L20). - [`ResourceStateEvent.create!`](https://gitlab.com/gitlab-org/gitlab/-/blob/c7e4ef1b65fc232f89f5b399a45d7b1e5942db25/lib/gitlab/github_import/importer/events/closed.rb#L37) won't disable [this validation](https://gitlab.com/gitlab-org/gitlab/-/blob/1d888461ae7a465b1c83c4d7aa6c75dbd6c43222/app/models/resource_state_event.rb#L7) ### Proposal Globals are an anti-pattern, but I think we could: - Implement a controlled form of a global where we set state within `Gitlab::SafeRequestStore` once. This is [available to Sidekiq workers](https://gitlab.com/gitlab-org/gitlab/-/blob/c4a9e01dc6fa517c042143bbfd08c2a3e524a541/lib/gitlab/sidekiq_middleware/request_store_middleware.rb#L7-9). - This would be read by the `Importable` module to know that we're importing. So we would "set and forget". **Example code** Just to sketch out the proposal in code: <details> <summary>Click to see semi-pseudo code</summary> ### Semi-pseudo code (untested): ```ruby module Import # Wrap the importing code in a block that ensured the state flag was unset def importing(&block) Gitlab::SafeRequestStore[:importing] = true yield ensure Gitlab::SafeRequestStore.delete(:importing) end # From Sidekiq, generally a worker will always be creating import data, # so we could probably just set it without needing to unset it def importing! raise "..." unless Gitlab::Runtime.sidekiq? Gitlab::SafeRequestStore[:importing] = true end end ``` </details> `Importable` module would be rewritten: ```diff diff --git a/app/models/concerns/importable.rb b/app/models/concerns/importable.rb index 4d2707b08abd..1ccc432e9820 100644 --- a/app/models/concerns/importable.rb +++ b/app/models/concerns/importable.rb @@ -4,8 +4,11 @@ module Importable extend ActiveSupport::Concern attr_accessor :importing - alias_method :importing?, :importing attr_accessor :imported alias_method :imported?, :imported + + def importing? + Gitlab::SafeRequestStore[:importing] || importing + end end ``` And we would use it like this: ```ruby # Rails/rake task: Import.importing do # ... create the import data end # Otherwise, from Sidekiq, generally a worker will always be creating # import data, so we could probably just set it within `#perform` so it's # not part of every stack trace. # We can block non-Sidekiq context code from being able to use this method. Import.importing! # ... create the import data ```
issue