Skip to content

Simplify proofing components#5737

Merged
zachmargolis merged 9 commits intomainfrom
margolis-simplify-proofing-components
Dec 21, 2021
Merged

Simplify proofing components#5737
zachmargolis merged 9 commits intomainfrom
margolis-simplify-proofing-components

Conversation

@zachmargolis
Copy link
Contributor

So as part of #5721, I looked at some of our proofing component code, and I think we can simplify it and use ActiveRecord built-in methods for some similar validation (such as checking if columns exist or not), and some slight query optimizations

This will conflict with #5721, so let's not merge this until that is wrapped up, but I did want to start a convo about it

Comment on lines 46 to 56
)
Db::ProofingComponent::Add.call(user_id, :document_check, session_doc_auth_vendor)
Db::ProofingComponent::Add.call(user_id, :document_type, 'state_id')
return unless liveness_checking_enabled?
Db::ProofingComponent::Add.call(user_id, :liveness_check, session_doc_auth_vendor)

proofing_component = current_user.proofing_component || current_user.build_proofing_component
component_attributes = {
document_check: session_doc_auth_vendor,
document_type: 'state_id',
}
if liveness_checking_enabled?
component_attributes[:liveness_check] = session_doc_auth_vendor
end
proofing_component.update(component_attributes)
end
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the old way, each of these 3 separate attributes resulted in a find-or-create + save, so that's like 2-3 queries x 3 attributes, and this is now 1 find + 1 save

return unless user_id
return unless TOKEN_ALLOWLIST.index(token.to_sym)

proofing_cost = ::ProofingComponent.create_or_find_by(user_id: user_id)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So arguably changing this to the user.proofing_component || user.build_proofing_component is more sensitive to race conditions than this existing code, I was not sure how to leverage the associations to do this same behavior

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option: user.proofing_component || user.create_proofing_component which shortens the time between queries

Copy link
Contributor

@aduth aduth Dec 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure I follow what practical impact we might expect from building vs. creating, but to your alternative, maybe we don't bother update-ing at all if the entry doesn't exist?

if current_user.proofing_component
  current_user.proofing_component.update(component_attributes)
else
  current_user.create_proofing_component(component_attributes)
end

Or assign with an upsert ?

current_user.proofing_component = ProofingComponent.upsert(
  user: current_user,
  **component_attributes,
)

Copy link
Contributor

@mitchellhenke mitchellhenke Dec 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous implementation was resilient to a race-condition where two conflicting records were created at the same time, and I think would be fine in the update case since we're only typically updating a column or two at a time. The current implementation I don't think accounts for either case.

Upsert would make the most sense imo both for performance and race-condition reasons.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that idea!

A few small snags:

  • from the upsert docs, it skips ActiveRecord callbacks, which means no updated_at and no created_at
[3] pry(main)> ProofingComponent.upsert(user_id: 1000)
D, [2021-12-21T09:25:36.713454 #44575] DEBUG -- :   ProofingComponent Upsert (16.5ms)  INSERT INTO "proofing_components" ("user_id") VALUES (1000) ON CONFLICT ("id") DO UPDATE SET updated_at=(CASE WHEN ("proofing_components"."user_id" IS NOT DISTINCT FROM excluded."user_id") THEN "proofing_components".updated_at ELSE CURRENT_TIMESTAMP END),"user_id"=excluded."user_id" RETURNING "id"
ActiveRecord::NotNullViolation: PG::NotNullViolation: ERROR:  null value in column "created_at" of relation "proofing_components" violates not-null constraint
DETAIL:  Failing row contains (6, 1000, null, null, null, null, null, null, null, null, null).
  • passing created_at to upsert would override an existing created_at which we don't want...
  • it doesn't return an AR model, so the current_user.proofing_component = part wouldn't work but we don't really need that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to keep noodling on this and see if I can find a way to do what we want

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated in b238e99

I had a few ideas:

  • switch the assocation to has_many :proofing_components (even though there's only one) because then we can do user.proofing_components.first_or_create
  • unfortunately that is not the same as create_or_find_by which still require the user to be passed in, so I just went with that

Copy link
Contributor

@aduth aduth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice simplification 👍

end

def save_proofing_components
return unless current_user
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we know when this would be the case? I was wondering about hybrid flow, where in other places we use the EffectiveUser mixin to manage the user associated with the anonymous session. But are we ever actually calling save_proofing_components in that hybrid session (i.e. do we need to bother with that mixin?)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am hoping it was just an artifact of how sloppily the individual feature tests for steps were done, it doesn't make sense why we need this check

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious if it's still needed with the recent round of revisions. Thinking the ProofingComponent.create_or_find_by would be more tolerant to the absence of current_user.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a null constraint on the column so this blows up:

[1] pry(main)> ProofingComponent.create_or_find_by(user: nil)
  TRANSACTION (0.1ms)  BEGIN
  ProofingComponent Create (2.4ms)  INSERT INTO "proofing_components" ("created_at", "updated_at") VALUES ($1, $2) RETURNING "id"  [["created_at", "2021-12-21 22:05:23.908635"], ["updated_at", "2021-12-21 22:05:23.908635"]]
  TRANSACTION (0.2ms)  ROLLBACK
ActiveRecord::NotNullViolation: PG::NotNullViolation: ERROR:  null value in column "user_id" of relation "proofing_components" violates not-null constraint
DETAIL:  Failing row contains (9, null, null, null, null, null, null, null, 2021-12-21 22:05:23.908635, 2021-12-21 22:05:23.908635, null).

return unless user_id
return unless TOKEN_ALLOWLIST.index(token.to_sym)

proofing_cost = ::ProofingComponent.create_or_find_by(user_id: user_id)
Copy link
Contributor

@aduth aduth Dec 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure I follow what practical impact we might expect from building vs. creating, but to your alternative, maybe we don't bother update-ing at all if the entry doesn't exist?

if current_user.proofing_component
  current_user.proofing_component.update(component_attributes)
else
  current_user.create_proofing_component(component_attributes)
end

Or assign with an upsert ?

current_user.proofing_component = ProofingComponent.upsert(
  user: current_user,
  **component_attributes,
)

@zachmargolis zachmargolis requested a review from aduth December 21, 2021 18:28
@zachmargolis
Copy link
Contributor Author

@mitchellhenke @aduth updated from comments! PTAL when you can

Copy link
Contributor

@aduth aduth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great 👍

end

def save_proofing_components
return unless current_user
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious if it's still needed with the recent round of revisions. Thinking the ProofingComponent.create_or_find_by would be more tolerant to the absence of current_user.

@zachmargolis zachmargolis merged commit 1e6a899 into main Dec 21, 2021
@zachmargolis zachmargolis deleted the margolis-simplify-proofing-components branch December 21, 2021 22:06
jmhooper pushed a commit that referenced this pull request Dec 28, 2021
* Inline Db::ProofingComponent::DeleteAll, use ActiveRecord methods
* Inline Db::ProofingComponent::Add, use AR methods
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants