Harden PostgreSQL concurrency, integrity, and error handling - #1023
Harden PostgreSQL concurrency, integrity, and error handling#1023Jet Chiang (supreme-gg-gg) wants to merge 9 commits into
Conversation
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
…in list ops, and replace snapshot tag creation precheck with fk Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
…tor RPC Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
…o fixes potential data loss on corrupt worker notifications Signed-off-by: Jet Chiang <pokyuen.jetchiang-ext@solo.io>
1d5c11f to
f547a2f
Compare
Tim Hockin (thockin)
left a comment
There was a problem hiding this comment.
I skimmed - this is not my expertise area, but a few thoughts
| REFERENCES atespaces(name) ON DELETE RESTRICT, | ||
| name text NOT NULL, | ||
| uid text NOT NULL UNIQUE, | ||
| uid text NOT NULL, |
There was a problem hiding this comment.
Should we include UID in every resource table? Not everything is updateable now, but that tends to not last, and we want consistent precondition semantics for every resource. This way we do not need to back-populate them when we inevitably need them later.
I see atespaces and actor_snapshots do not have UID
There was a problem hiding this comment.
actor_snapshots should be immutable, so it should not need UID for update? I can add version and UID to atespaces in case it will become updatable
There was a problem hiding this comment.
Immutability tends to erode, unless there's a clear reason not to we should probably just be consistent?
|
|
||
| // updateMaxAttempts bounds how many times a read-modify-write is retried after | ||
| // its optimistic uid/version check loses to a concurrent writer. | ||
| const updateMaxAttempts = 5 |
There was a problem hiding this comment.
Why do we retry at all if there are preconditions specified on input? If I asked for version==6 and I find DB has version 7, no retries will ever fix it. If we support non-precondition updates, this could matter.
There was a problem hiding this comment.
The current update interface does support non-precondition updates, so the retry loop intends to support unpinned mutations which wish to reapply the change to the latest stored value, there is also a test in atepg_test.go that exhibits this behaviour. You're right about version-pinned updates, they will return with ErrVersionConflict after a failed CAS.
I just double checked that all ateapi callers are pinned using WithPrecondition, so we might want to remove the retry loop and make the update interface always pinned. However, this seems to me to be partially reversing #763 which replaced expectedVersion with a transactional mutate closure.
Julian Gutierrez Oschmann (@juli4n) is the goal of #763 to support retryable, unpinned read-modify-writes?
There was a problem hiding this comment.
If we end up keeping the retry loop, recommend adding a small amount of jittered backoff. Without it this is a relatively hot loop that expires quickly. More bang for the buck if you spread the requests over a bit more time.
There was a problem hiding this comment.
Julian's proposal for update is:
- Whole-object updates, require preconditions now
- Maybe whole-object updates without preconditions later
- Probably patch-updates without preconditions later
I'm fine to keep the loop now as long as we only loop when there are no preconditions in the user input. Note that (I think?) a patch is still:
user calls patch with no preconditions
loop
read old value
call mutate()
submit to storage with preconditions
if success; break loop
?
| // its optimistic uid/version check loses to a concurrent writer. | ||
| const updateMaxAttempts = 5 | ||
|
|
||
| func validateMetadataProjection(resource string, metadata *ateapipb.ResourceMetadata, uid string, version int64) error { |
There was a problem hiding this comment.
Is Projection a term I don't know or did you mean Precondition?
There was a problem hiding this comment.
Here "projection" refers to the version/uid columns "projected" from the stored proto, which is different from precondition which refers to caller provided expectations. I agree it's a bit unfamiliar, I'll change it to something like validateProtoMetadataMatchesColumns, which is more literal.
There was a problem hiding this comment.
Thanks! A comment would suffice.
Joe Betz (jpbetz)
left a comment
There was a problem hiding this comment.
Thanks for getting this all addressed, this all looks like it's moving in the right direction to me. A few minor comments and I saw Tim Hockin (@thockin) had some comments, after that this looks ready to me.
| func (p *Persistence) AcquireLock(ctx context.Context, key string) (*store.Lock, error) { | ||
| ttl := p.lockTTL | ||
| token := uuid.NewString() | ||
| if err := p.cleanupExpiredLeases(ctx); err != nil { |
There was a problem hiding this comment.
Recommend opening an issue to move this to a dedicated cleanup task for the long term. It's not super urgent, but it's worth tracking and circling back on.
| parent := resources.ActorTemplateRefFromObjectRef(atv.GetActorTemplate()) | ||
| exists, err := s.ActorTemplateExists(ctx, parent) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("while checking actor template parent %s: %w", parent, err) | ||
| } | ||
| if !exists { | ||
| return nil, store.ErrFailedPrecondition |
There was a problem hiding this comment.
Consider adding a comment that is best-effort but orphans here are possible.
This PR makes improvements to the Postgres store by addressing review comments on #940 and optimizing Postgres updates as suggested in #988.