Feature: conditional upload API#178
Conversation
…, inmem providers Signed-off-by: Tom Plowman <7210552+alsenz@users.noreply.github.com>
Signed-off-by: Tom Plowman <7210552+alsenz@users.noreply.github.com>
Signed-off-by: Tom Plowman <7210552+alsenz@users.noreply.github.com>
Signed-off-by: Tom Plowman <7210552+alsenz@users.noreply.github.com>
c21b930 to
1bb876b
Compare
|
@bwplotka - would you be the appropriate maintainer to submit this for a first review? Greatly appreciated. |
| } | ||
| } | ||
|
|
||
| func tryOpenFile(name string, ifNotExists bool) (exists bool, err error) { |
| } | ||
|
|
||
| // ValidateUploadOptions ensures that only supported options are passed as options. | ||
| func ValidateUploadOptions(supportedOptions []ObjectUploadOptionType, opts ...ObjectUploadOption) error { |
There was a problem hiding this comment.
maybe we should also validate IfNotExists and IfMatch/IfNotMatch aren't used together
| b.mtx.Lock() | ||
| defer b.mtx.Unlock() | ||
|
|
||
| params := ApplyObjectUploadOptions(opts...) |
|
What's the progress on this PR? Any chance it gets merged? |
Signed-off-by: Alsenz <7210552+alsenz@users.noreply.github.com>
Signed-off-by: Tom Plowman <7210552+alsenz@users.noreply.github.com>
a5d77ae to
b395c1c
Compare
Signed-off-by: Tom Plowman <7210552+alsenz@users.noreply.github.com>
|
Thanks @yuchen-db for your review. I agree with all your points and have pushed some changes accordingly. This is an old PR but hopefully we can get it across the line. |
|
@anarcher this is an older PR that didn't get much attention, hopefully the maintainers will be able to find some time to review and accept. I have brought the feature branch back in line with main and responded to comments, so it should be ready to go. |
Signed-off-by: Tom Plowman <7210552+alsenz@users.noreply.github.com>
Signed-off-by: Tom Plowman <7210552+alsenz@users.noreply.github.com>
cbdcc69 to
9ee1894
Compare
Signed-off-by: Tom Plowman <7210552+alsenz@users.noreply.github.com>
|
Would like to see this PR merged soon :) |
|
I'll try to take a look no later than 2 months~ from now |
| return ctx.Err() | ||
| } | ||
|
|
||
| file := filepath.Join(b.rootDir, name) |
There was a problem hiding this comment.
Why can't we create an extended attribute on the same file?
There was a problem hiding this comment.
This was written a while ago, so apologies if I'm a little hazy, but I'm fairly use we're using the swap-and-move idiom to prevent race conditions when the file is created but the xattrs aren't written, which would could cause versioning assumptions to go wrong- this relies on atomic move of course but that's already ensured by SupportedObjectUploadOptions - we don't do this code path if we don't have xattrs and atomic moves.
To double check, I ran this through claude (which I try to avoid relying on but is a good double-check), it said:
▎ The xattr is set on the swap file before the atomic os.Rename. Since os.Rename on Unix is atomic and xattrs are stored as inode metadata, they travel with the file on rename. Setting the xattr on file after the
rename would introduce a race: another goroutine could call checkConditions in that window and see the file without its version data.
| return false | ||
| } | ||
|
|
||
| // IsConditionNotMetErr returns true if the response status code was Precondition Failed or Not Modified. |
There was a problem hiding this comment.
The same here: I suggest not adding into the comments what is already in the code.
There was a problem hiding this comment.
Agreed - will fix in next commit
| if uploadOpts.IfNotExists { | ||
| putOpts.SetMatchETagExcept("*") | ||
| } else if uploadOpts.Condition != nil { | ||
| // If-None-Match with header values other than "*" is not supported by AWS yet. |
There was a problem hiding this comment.
Should we error out on this condition? How the user would know about this? The user could pass something and it would do something different than what is expected so I would expect an error here.
There was a problem hiding this comment.
Absolutely - yes, good catch! Will fix in next commit.
|
@alsenz ping |
|
@GiedriusS just seen this thanks for the review (been a while sorry). I will resolve these tonight after work |
Co-authored-by: Giedrius Statkevičius <giedriuswork@gmail.com> Signed-off-by: Alsenz <7210552+alsenz@users.noreply.github.com>
Signed-off-by: Tom Plowman <7210552+alsenz@users.noreply.github.com>
|
@GiedriusS -- PR comments addressed - haven't resolved the comments yet until you take a look. my best. |
GiedriusS
left a comment
There was a problem hiding this comment.
Thank you, a few more suggestions
| // Upload should be idempotent. | ||
| Upload(ctx context.Context, name string, r io.Reader, opts ...ObjectUploadOption) error | ||
| Upload(ctx context.Context, name string, r io.Reader, options ...ObjectUploadOption) error | ||
|
|
There was a problem hiding this comment.
Some random whitespace issues here.
|
|
||
| // ObjectUploadOption configures UploadObjectParams. | ||
| type ObjectUploadOption struct { | ||
| Type ObjectUploadOptionType |
There was a problem hiding this comment.
I don't like that these two are disjointed now and users can misuse this ie create some ObjectUploadOption that doesn't do what Type says it should do. Should we make Type and Apply hidden i.e. lowercase so that users wouldn't be able to do that?
There was a problem hiding this comment.
No fair enough, although I think there may be other places where symbols are overexposed maybe. Either way, easy change to make and very happy to do.
| } | ||
|
|
||
| func tryOpenFile(name string, ifNotExists bool) (exists bool, err error) { | ||
| // First try to open the file with exclusive create, then truncate if permitted |
There was a problem hiding this comment.
Where truncation happens here? 🤔
There was a problem hiding this comment.
No idea. I think it's a comment typo rather than code typo, re-readin gthe whole logic.
There was a problem hiding this comment.
I'll just remove the comment fo rnow
| if _, err := io.Copy(writer, r); err != nil { | ||
| return errors.Wrapf(err, "copy to %s", swap) | ||
| } | ||
| // Write the checksum into an xattr |
There was a problem hiding this comment.
I suggest removing self-explanatory comments like this.
| return err | ||
| } | ||
|
|
||
| if xattr.XATTR_SUPPORTED { |
There was a problem hiding this comment.
If this is false then where are we writing to swf?
There was a problem hiding this comment.
This is a bug- thanks - think the fix is a few lines, will fix.
|
|
||
| // ValidateUploadOptions ensures that only supported options are passed as options, and that options used simultaneously are valid. | ||
| func ValidateUploadOptions(supportedOptions []ObjectUploadOptionType, opts ...ObjectUploadOption) error { | ||
|
|
There was a problem hiding this comment.
Typically we don't put a empty line just after the definition.
| // Upload writes the file specified in src to into the memory. | ||
| func (b *Bucket) Upload(ctx context.Context, name string, r io.Reader, _ ...objstore.ObjectUploadOption) (err error) { | ||
| func (b *Bucket) Upload(ctx context.Context, name string, r io.Reader, opts ...objstore.ObjectUploadOption) (err error) { | ||
|
|
|
Thanks for the second pass @GiedriusS - will apply changes tonight and push. |
Signed-off-by: Tom Plowman <7210552+alsenz@users.noreply.github.com>
| } | ||
|
|
||
| func (t TracingBucket) IsConditionNotMetErr(err error) bool { | ||
| return t.bkt.IsAccessDeniedErr(err) |
There was a problem hiding this comment.
Should this be IsConditionNotMetErr?
| file := filepath.Join(b.rootDir, name) | ||
| bytes, err := xattr.Get(file, xAttrKey) | ||
| if err != nil { | ||
| return "", err // Legacy filesystem buckets would just return empty string for the version (until objects updated). |
There was a problem hiding this comment.
According to the comment, shouldn't nil error be returned?
| // Upload writes the file specified in src to into the memory. | ||
| func (b *Bucket) Upload(ctx context.Context, name string, r io.Reader, _ ...objstore.ObjectUploadOption) (err error) { | ||
| func openSwap(name string) (swf *os.File, err error) { | ||
| for { |
There was a problem hiding this comment.
Why is this a loop? If creating this fails for some reason other than fs.ErrExist then this will loop indefinitely 🤔
| if f == nil { | ||
| return | ||
| } | ||
| err = f.Close() |
There was a problem hiding this comment.
This is immediately closed so maybe we can just call os.Stat instead?
Resolves #129
Adds support for conditional object uploads.
Extends
ObjectAttributeswith optionalObjectVersion(if supported), andUpload(...)with three newObjectUploadOption, depending on provider support.IfMatch: the object is written only if the existing object matches the provided versionIfNotMatch: the object is written only if the existing object does not match the provided versionIfNotExists: the object is written only if no object already exists for the provided keySupports two forms of
ObjectVersion:Not all providers support conditional write. The following providers are supported:
IfNotMatchis not yet supported (as it is not supported by AWS).Clients can check conditional API by calling
SupportedObjectUploadOptionson theBucketinterface.Changes
API changes
ObjectUploadOptions:IfMatch,IfNotMatchandIfNotExists1.1 Made
ObjectUploadOptiontyped, repeating the existingIterOptionpattern.1.2 Added three new parameter modifiers-
WithIfMatch,WithIfNotExistsandWithIfNotMatch, andWithContentTypeto support the previous un-typedObjectUploadOptionObjectVersionfield toObjectAttributesIfMatch,IfNotMatch,IfNotExistsand forAttributesAPIBucketprovider implementations for:1.1 filesystem: this now uses Extended Attributes, if supported by the host system
1.2 inmem
1.3 GCS
1.4 Azure
1.5 S3, excepting
IfNoneMatchwhich is not fully supported (by AWS) yet1.6 Wrappers
Backwards compatibility
API changes are additive and should be backwards compatible.
The
UploadAPI changes use a trailing variadic parameter so should be backwards compatibleThe
ObjectAttributesadd new field so should be backwards compatibleThe
SupportedObjectUploadOptionsinterface method potentially breaks existingBucketimplementations. Library users who implement their own providers will need to implement this method. Full or stub implementations are provided for all provider implementations in the libary.Dependencies
Verification
Added acceptance tests for
IfMatchIfNotMatchIfExistsDid not run integration tests on other object store providers, as I do not have access to test environments, but these should be unchanged.