-
Notifications
You must be signed in to change notification settings - Fork 71
THREESCALE-12434: Migrate from protected attributes to strong parameters - Part 3 #4255
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
base: strong-params-part2
Are you sure you want to change the base?
Changes from all commits
5e08ec1
8d4cce0
a679409
6321284
8e93ab0
a0da47d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,7 +33,11 @@ def backend_api_config | |
| end | ||
|
|
||
| def backend_api | ||
| @backend_api ||= backend_api_configs.first&.backend_api || account.backend_apis.build(system_name: service_system_name, name: "#{service_name} Backend", description: "Backend of #{service_name}") | ||
| # Return early if we already have a persisted backend_api | ||
| return @backend_api if @backend_api&.persisted? | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I further changed this one, and also added a different This resolves issues in some tests, where the following happens:
The previous solution was to do this in the test factories: But I really didn't like it, it felt very hacky. The current solution (while also a bit hacky), is better, I think, because we don't create something that we remove later, we avoid creating it in the first place. Once this lazily built object is saved, the association that is created is the same.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This tricky logic should be tested probably. Maybe a simple set of unit tests would be enough.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice!. Surprisingly, my previous approval is still there after adding new commits. |
||
|
|
||
| # Try to get backend_api from backend_api_configs, or keep the memoized unpersisted one, or build a new one | ||
| @backend_api = backend_api_configs.first&.backend_api || @backend_api || build_backend_api | ||
| end | ||
|
|
||
| def update!(attrs = {}) | ||
|
|
@@ -52,6 +56,18 @@ def update(attrs = {}) | |
| rescue ActiveRecord::RecordInvalid | ||
| false | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def build_backend_api | ||
| # Build without adding to association to avoid polluting it with unpersisted records | ||
| BackendApi.new( | ||
| account: account, | ||
| system_name: service_system_name, | ||
| name: "#{service_name} Backend", | ||
| description: "Backend of #{service_name}" | ||
| ) | ||
| end | ||
| end | ||
|
|
||
| def backend_api_proxy | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,10 +32,8 @@ def diff(other) | |
| end | ||
|
|
||
| def revert_attributes | ||
| accessible = template.class.accessible_attributes.delete('draft').add('updated_at').add('updated_by') | ||
|
|
||
| accessible << state.to_s | ||
| attributes.slice *accessible | ||
| accessible = [:updated_at, :updated_by, state.to_s] | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't remember how exactly I came up with this replacement, but I think it was just debugging and checking what fields were actually available there.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to Claude, I broke this method in this commit: 33b70d3 However, after my changes it was effectively doing the same you do in this commit, just returning So I guess:
There are no tests for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm... I don't see changes to that file in the commit you mentioned 🤔
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In that commit, I removed the protected attributes for the - attr_accessible :provider, :draft, :liquid_enabled, :handlerAfter that change,
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I, I see, right, thank you for the explanation! Yeah, it rings a bell now, indeed, that
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think the versioning feature is working fine? didn't I break it?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When I tried, it seemed to be doing the right thing.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A good engineer knows how to make mistakes! |
||
| attributes.slice(*accessible) | ||
| end | ||
|
|
||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,14 +10,15 @@ def self.fetch_with_retry!(options, &block) | |
| retries = 0 | ||
|
|
||
| begin | ||
| MailDispatchRule.find_or_create_by!(options, &block) | ||
| # Use find_by + create! directly to avoid transaction wrapper from create_or_find_by! | ||
| # This preserves the old behavior from protected_attributes_continued gem | ||
| # where records created in the block are committed even if the outer create fails | ||
| find_by(options) || create!(options, &block) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure whether this is the right thing to do... This is basically keeping the old behavior. The test that is failing without it is: It verifies (how I understand it) that if a race condition occurs when creating With no code change, the test fails with the error: So, with With the gem removed, the ActiveRecord method is being called directly: create_or_find_by! wraps the createion in a transaction, so if anything fails within it, all created objects get rolled back.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, honestly, my feeling is that it's that the test doesn't represent what would be happening in the production code. If the object is created in two different places, then Rails' standard So, we probably don't need this loop, and the race condition simulation should be done in a different way. However, I don't want to risk it, because I don't quite understand the possible use case, so I decided to just keep the previous behavior.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I don't remember all details now, but I think any changes there will not have any effect.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found this: https://redhat.atlassian.net/browse/THREESCALE-11825
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good, thank you for the info! |
||
| rescue ActiveRecord::RecordNotUnique | ||
| if retries > 10 | ||
| raise | ||
| else | ||
| retries += 1 | ||
| retry | ||
| end | ||
| raise if retries > 10 | ||
|
|
||
| retries += 1 | ||
| retry | ||
| end | ||
| end | ||
| end | ||
This file was deleted.
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 issue with
#backend_apijust drives me nuts.This commit contains a solution that Claude provided, and I'm putting his explanation below.
I am not convinced or happy with this though, and I find the whole lazy building messy... I think we need to review the behavior again, and try to get rid of any lazy building, because it just makes things complicated and error-prone.
WDYT @akostadinov @jlledom ?
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.
BackendApiProxy Cache Issue After Removing protected_attributes_continued
Problem
After removing the
protected_attributes_continuedgem (strong-params-part3 branch), several testsstarted failing with errors like:
NoMethodError: undefined method 'children' for nil(whenservice.backend_apireturns a staleunpersisted BackendApi with no metrics)
StateMachines::InvalidTransition: Cannot transition state via :approve from :created (Reason(s): Backend apis invalid)(when account validation encounters the unpersisted BackendApi)Root Cause
The behavioral difference is caused by
inverse_ofworking correctly withoutprotected_attributes_continued.How BackendApiProxy caches a backend_api
When a Service is created,
after_create :create_default_proxybuilds and saves a Proxy. DuringProxy validation,
validate_backend_api?(inBackendApiLogic::ProxyExtension) callsbackend_api.private_endpoint_changed?. This delegates through:BackendApiProxy#backend_apidoes:Since no
backend_api_configsexist yet during initial service creation, it falls through toaccount.backend_apis.build(...), which creates an unpersisted BackendApi (noprivate_endpoint,no metrics) and caches it in
@backend_api.What changed: inverse_of behavior
The Proxy model has
belongs_to :service, touch: true, inverse_of: :serviceand the Service modelhas
has_one :proxy, dependent: :destroy, inverse_of: :service, autosave: true.On master (with protected_attributes_continued)
The gem subtly breaks
inverse_offorhas_oneassociations. When a Proxy is created viaservice.create_proxy!, the Proxy'sservicemethod resolves to a different Service instance(loaded from DB), not the original one.
This means the stale
@backend_api_proxy(with its cached unpersisted BackendApi) is set on athrowaway Service instance that gets garbage collected. The FactoryBot-returned Service instance
has a clean
@backend_api_proxy— callingservice.backend_apicreates a freshBackendApiProxythat correctly finds the persisted BackendApi from
backend_api_configs.On the branch (without the gem)
inverse_ofworks correctly. The Proxy'sservicemethod returns the same Service instancethat created it. The stale
@backend_api_proxy(with its cached unpersisted BackendApi) is set onthe FactoryBot-returned Service instance.
Later calls to
service.backend_apireturn the stale unpersisted BackendApi instead of the realpersisted one created by the
:with_default_backend_apifactory trait.Evidence
Tracing
BackendApiProxy#backend_apicalls duringFactoryBot.create(:simple_service):Master:
Branch:
Impact on Production Code
The main production code path that uses
service.backend_apiisServiceCreator:ServiceCreatoris not affected because:backend_api_proxy.update!setsprivate_endpointon it and callssave!, persisting itBackendApiProxyinstance is used throughout, so it operates on the sameBackendApi object
Other production code paths access backend_api through
BackendApiConfigassociations (not throughthe
BackendApiProxycache), so they are also unaffected.Fix
Code fix:
BackendApiProxy#backend_apiChanged to handle stale caches by re-checking
backend_api_configswhen the cached value isunpersisted:
Logic:
backend_api_configsfirst (picks up configs createdafter initial cache, e.g. by
:with_default_backend_apifactory trait)instances via
account.backend_apis.build, which would pollute the association)This is safe for
ServiceCreatorbecause step 3 reuses the same unpersisted BA across callswithin
update!.Factory fix: service factories
Both
:simple_serviceand:servicefactories clean up the account'sbackend_apisassociationin
after(:create):This is needed because
account.backend_apis.build(...)(called during proxy validation) adds anunpersisted BackendApi to the account's loaded association. If the account is later validated (e.g.,
during
approve!in the provider signup flow), this unpersisted BA fails validation because it hasno
private_endpoint. The code fix inBackendApiProxy#backend_apihandles the separate issue ofthe stale cache but does not remove the unpersisted BA from the association.
Uh oh!
There was an error while loading. Please reload this page.
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 don't see it so problematic, maybe a comment might help, but if you tried and it works, I think its fine.
Maybe a clearer refactor (maybe not):