Skip to content
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

[db] Audit usage of "ON CONFLICT DO NOTHING RETURNING ..." #1168

Open
Tracked by #4189
smklein opened this issue Jun 7, 2022 · 5 comments
Open
Tracked by #4189

[db] Audit usage of "ON CONFLICT DO NOTHING RETURNING ..." #1168

smklein opened this issue Jun 7, 2022 · 5 comments
Labels
database Related to database access

Comments

@smklein
Copy link
Collaborator

smklein commented Jun 7, 2022

(here's some background that covers this use-case)

There are a handful of spots in the datastore where we:

  • INSERT INTO a table,
  • ON CONFLICT, DO NOTHING
  • and have a RETURNING clause

Example:

let instance: Instance = diesel::insert_into(dsl::instance)
.values(instance)
.on_conflict(dsl::id)
.do_nothing()
.returning(Instance::as_returning())

This is valid SQL, but it's a little suspect:

  • If there are no conflicts (we're inserting the object with the UUID for the first time) we get the value that we intended to insert.
  • However, if there are conflicts, this group of statements does not return any rows. Instead, it results in a NotFound error from the database.

This means that, especially in situations where we expect idempotency, this statement may return different results if invoked multiple times.

@smklein
Copy link
Collaborator Author

smklein commented Dec 7, 2022

Avoiding do_nothing(), and replacing it with do_update().set(...), for any field, seems to be a reasonable workaround?

@davepacheco
Copy link
Collaborator

Maybe? The top answer in the linked Stackoverflow post explains a bunch of unfortunate side effects of that, though some of those may not apply in CockroachDB.

I feel like we had a chunk of code for doing exactly this a long time ago. Does that ring a bell?

@smklein
Copy link
Collaborator Author

smklein commented Dec 8, 2022

I googled "ON CONFLICT DO NOTHING RETURNING" and found this; I'm assuming this is what you're referencing?

This answer mentions:

The [approach of updating anyway] seems ok for a single conflict target, few conflicts, small tuples and no triggers.

  • Our conflict target is nearly always a UUID, so it's unique
  • So there are few conflicts
  • This data is relatively small
  • CRDB is not using triggers

I do not recall of any code to help in this case of "insert or read the record idempotently".

Their proposal of:

  • Use a CTE to generate all the potential values to be inserted
  • Attempt to INSERT into the table, DO NOTHING on return
  • SELECT the values which were potentially inserted, and...
  • ... UNION those values with all the potential values to be inserted

Definitely could also work, and avoid the write contention.

They mention:

The query itself (not counting the side effects) may be a bit more expensive for few dupes, due to the overhead of the CTE and the additional SELECT (which should be cheap since the perfect index is there by definition - a unique constraint is implemented with an index).

FWIW, in our case, we would expect exactly one duplicate.

I do think avoiding any increased write traffic seems like a good goal, but implementing this CTE, including all the necessary type-casting, will probably take a good deal of time to implement for a general case. In the meantime, if these calls are used anywhere that idempotency is expected (e.g., all our saga calls), it's producing wrong results.

We can avoid the do_update().set(...) policy if you're concerned, but I fear that we may avoiding the "optimal" solution in favor of one that is actively wrong.

@davepacheco
Copy link
Collaborator

I googled "ON CONFLICT DO NOTHING RETURNING" and found this; I'm assuming this is what you're referencing?

Yeah, sorry, I should have been clearer. This is the link I was talking about. It's the one you included in this issue description.

I do not recall of any code to help in this case of "insert or read the record idempotently".

Ah, I think I was thinking of sql_insert_unique_idempotent_and_fetch(). This went out with PR #192. While looking for this I found I referenced this same StackOverflow answer in RFD 192 as the inspiration for the conditional update CTE. I think some of this probably went into collection_insert...but that's not general enough for what you need here because it also does the collection generation counter stuff. So, nevermind!

I do think avoiding any increased write traffic seems like a good goal, but implementing this CTE, including all the necessary type-casting, will probably take a good deal of time to implement for a general case. In the meantime, if these calls are used anywhere that idempotency is expected (e.g., all our saga calls), it's producing wrong results.

We can avoid the do_update().set(...) policy if you're concerned, but I fear that we may avoiding the "optimal" solution in favor of one that is actively wrong.

👍

@smklein
Copy link
Collaborator Author

smklein commented Dec 9, 2022

Ah, I think I was thinking of sql_insert_unique_idempotent_and_fetch(). This went out with PR #192. While looking for this I found I referenced this same StackOverflow answer in RFD 192 as the inspiration for the conditional update CTE. I think some of this probably went into collection_insert...but that's not general enough for what you need here because it also does the collection generation counter stuff. So, nevermind!

I looked at this implementation to see if I could re-use it, but AFAICT it's doing two operations:

  • Inserting with an ON CONFLICT <user provided conflict string> DO NOTHING
  • Fetching the row as a follow-up operation

So it's not atomic, and I think there's a comment about this:

/// The insert + fetch sequence is not atomic. It's conceivable that another
/// client could modify the record in between. The caller is responsible for
/// ensuring that doesn't happen or does not matter for their purposes. In
/// practice, the surrounding saga framework should ensure that this doesn't
/// happen.

(I'm not sure how sagas should help enforce atomicity?)

This operation is easy to do in diesel today; it's basically just:

let result = diesel::insert_into(table)
  .values(values)
  .on_conflict(...)
  .do_nothing()
  .get_result_async()
  .await?;

match result {
  Err(NotFound) => {
    return table.filter(id.eq(id)).get_result_async().await;
  }
  _ => return result;
}

But I think it's the "making it a single atomic operation" part that would be tricky.

smklein added a commit that referenced this issue Dec 29, 2022
- Makes use of oxidecomputer/steno#88 to test
idempotency of instance creation actions + undo actions
- Fixes all the ways in which it was *not* idempotent, including...
- (Action) Inserting the same network interface failed with a VPC subnet
error, instead of an "already exists" error.
- (Action) Inserting the instance record twice fell victim to
#1168
- (Undo) Deleting the instance record failed to execute twice, as it
could not find the instance record for the second invocation.

Part of #2094
@smklein smklein mentioned this issue Jan 18, 2023
20 tasks
leftwo pushed a commit that referenced this issue Mar 4, 2024
Crucible changes:
Per client, queue-based backpressure (#1186)
A builder for the Downstairs Downstairs struct. (#1152)
Update Rust to v1.76.0 (#1153)
Deactivate the read only parent after a scrub (#1180)
Start byte-based backpressure earlier (#1179)
Tweak CI scripts to fix warnings (#1178)
Make `gw_ds_complete` less public (#1175)
Verify extent under repair is valid after copying files (#1159)
Remove individual panic setup, use global panic settings (#1174)
[smf] Use new zone network config service (#1096)
Move a few methods into downstairs (#1160)
Remove extra clone in upstairs read (#1163)
Make `crucible-downstairs` not depend on upstairs (#1165)
Update Rust crate rusqlite to 0.31 (#1171)
Update Rust crate reedline to 0.29.0 (#1170)
Update Rust crate clap to 4.5 (#1169)
Update Rust crate indicatif to 0.17.8 (#1168)
Update progenitor to bc0bb4b (#1164)
Do not 500 on snapshot delete for deleted region (#1162)
Drop jobs from Offline downstairs. (#1157)
`Mutex<Work>` → `Work` (#1156)
Added a contributing.md (#1158)
Remove ExtentFlushClose::source_downstairs (#1154)
Remove unnecessary mutexes from Downstairs (#1132)

Propolis changes:
PHD: improve Windows reliability (#651)
Update progenitor and omicron deps
Clean up VMM resource on server shutdown
Remove Inventory mechanism
Update vergen dependency
Properly handle pre/post illumos#16183 fixups
PHD: add `pfexec` to xtask phd-runner invocation (#647)
PHD: add Windows Server 2016 adapter & improve WS2016/2019 reliability (#646)
PHD: use `clap` for more `cargo xtask phd` args (#645)
PHD: several `cargo xtask phd` CLI fixes (#642)
PHD: Use ZFS clones for file-backed disks (#640)
PHD: improve ctrl-c handling (#634)
leftwo added a commit that referenced this issue Mar 4, 2024
Crucible changes:
Per client, queue-based backpressure (#1186)
A builder for the Downstairs Downstairs struct. (#1152) Update Rust to
v1.76.0 (#1153)
Deactivate the read only parent after a scrub (#1180) Start byte-based
backpressure earlier (#1179)
Tweak CI scripts to fix warnings (#1178)
Make `gw_ds_complete` less public (#1175)
Verify extent under repair is valid after copying files (#1159) Remove
individual panic setup, use global panic settings (#1174) [smf] Use new
zone network config service (#1096)
Move a few methods into downstairs (#1160)
Remove extra clone in upstairs read (#1163)
Make `crucible-downstairs` not depend on upstairs (#1165) Update Rust
crate rusqlite to 0.31 (#1171)
Update Rust crate reedline to 0.29.0 (#1170)
Update Rust crate clap to 4.5 (#1169)
Update Rust crate indicatif to 0.17.8 (#1168)
Update progenitor to bc0bb4b (#1164)
Do not 500 on snapshot delete for deleted region (#1162) Drop jobs from
Offline downstairs. (#1157)
`Mutex<Work>` → `Work` (#1156)
Added a contributing.md (#1158)
Remove ExtentFlushClose::source_downstairs (#1154) Remove unnecessary
mutexes from Downstairs (#1132)

Propolis changes:
PHD: improve Windows reliability (#651)
Update progenitor and omicron deps
Clean up VMM resource on server shutdown
Remove Inventory mechanism
Update vergen dependency
Properly handle pre/post illumos#16183 fixups
PHD: add `pfexec` to xtask phd-runner invocation (#647) PHD: add Windows
Server 2016 adapter & improve WS2016/2019 reliability (#646) PHD: use
`clap` for more `cargo xtask phd` args (#645) PHD: several `cargo xtask
phd` CLI fixes (#642)
PHD: Use ZFS clones for file-backed disks (#640)
PHD: improve ctrl-c handling (#634)

Co-authored-by: Alan Hanson <alan@oxide.computer>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
database Related to database access
Projects
None yet
Development

No branches or pull requests

2 participants