-
Notifications
You must be signed in to change notification settings - Fork 402
Implement graveler condition functionality #9566
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: master
Are you sure you want to change the base?
Conversation
📚 Documentation preview at https://pr-9566.docs-lakefs-preview.io/ (Updated: 10/8/2025, 1:52:05 PM - Commit: cc7ea2b) |
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.
Good solution for a tricky problem.
Requesting changes mainly since this is a delicate part of the code and I'd be happy to make it more readable to someone who isn't deep in the details -
But most of my comments are about it's about style or nit.
"github.com/go-openapi/swag" | ||
"github.com/gorilla/sessions" | ||
authacl "github.com/treeverse/lakefs/contrib/auth/acl" | ||
"github.com/treeverse/lakefs/modules/api/factory" |
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.
The convention is:
apifactory "github.com/treeverse/lakefs/modules/api/factory"
} | ||
} | ||
|
||
type ConditionFunc func(currentValue *Value) error |
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.
type ConditionFunc func(currentValue *Value) error | |
type ConditionFunc func(currentValue *Value) error | |
|
||
// BuildConditionFromParams creates a graveler.ConditionFunc from upload params. | ||
// Returns nil if no precondition is specified in the params. | ||
func BuildConditionFromParams(params apigen.UploadObjectParams) *graveler.ConditionFunc { |
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.
Why it's in the api
factory and not in the catalog
factory?
Asking since it creates a graveler
dependency here, where in the catalog
package it already exists.
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.
controller (pkg not just module) is exposed to graveler, on the other hand catalog is not exposed to apigen due to abstraction.
There's no perfect solution here but I believe this is the better one
// EntryCondition adapts an Entry-level condition function to a graveler.ConditionFunc. | ||
// It converts graveler Values to Entries before applying the condition, enabling Entry-based | ||
// validation logic (e.g., Object metadata checks) to work with graveler's conditional operations. | ||
func EntryCondition(conditionFunc func(*Entry) error) graveler.ConditionFunc { |
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.
Nit: name the param condition
instead of conditionFunc
.
- Its type is already
func
, so it's redundant. - If you insist naming it
conditionFunc
, soEntryCondition
should also be calledEntryConditionFunc
, since it's basically a wrapper doing a similar action, so should have a similar name.
log := g.log(ctx).WithFields(logging.Fields{"key": key, "operation": "set"}) | ||
err = g.safeBranchWrite(ctx, log, repository, branchID, safeBranchWriteOptions{MaxTries: options.MaxTries}, func(branch *Branch) error { | ||
if !options.IfAbsent { | ||
// Check if Condition is provided |
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.
Nit: you're not actually checking it here.
// Check if Condition is provided | ||
hasCondition := options.Condition != nil | ||
|
||
if !options.IfAbsent && !hasCondition { |
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.
Why IfAbsent
is related to hasCondition
?
// update stage with new value respecting the ifAbsent and condition | ||
return g.StagingManager.Update(ctx, branch.StagingToken, key, func(stagingValue *Value) (*Value, error) { | ||
var valueToCheck *Value | ||
noValue := stagingValue == nil || stagingValue.Identity == nil |
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.
Why this is a var, and not part of the case
?
switch { | ||
case noValue: | ||
// Nothing in staging, check against committed value | ||
valueToCheck = curValue |
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.
Why not return here?
Is options.Condition()
relevant for nil values?
return g.StagingManager.Update(ctx, branch.StagingToken, key, func(currentValue *Value) (*Value, error) { | ||
if currentValue == nil || currentValue.Identity == nil { | ||
return &value, nil | ||
// update stage with new value respecting the ifAbsent and condition |
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.
// update stage with new value respecting the ifAbsent and condition | |
// update stage with new value respecting the IfAbsent and Condition |
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.
(The nittest nit, ofc.)
} | ||
|
||
// Run condition if provided | ||
if hasCondition { |
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.
It would be clearer to me if this was part of the switch
, right after checking the IfAbsent
. It would be a list of conditions to check.
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.
Thanks!
Added my comments - did not go over tests as I believe the changes required are going to modify them
|
||
var setOpts []graveler.SetOptionsFunc | ||
// Handle If-Match precondition | ||
if condition := factory.BuildConditionFromParams(params); condition != nil { |
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.
BuildConditionFromParams should return an error.
Since the logic and contents of params is an opaque for the controller the method should also validate the params and return error in case of conflict, bad values, etc.
} | ||
|
||
var setOpts []graveler.SetOptionsFunc | ||
// Handle If-Match precondition |
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 comment is irrelevant for this code and will quickly become obsolete and confusing.
entry := entryBuilder.Build() | ||
|
||
err = c.Catalog.CreateEntry(ctx, repo.Name, branch, entry, graveler.WithIfAbsent(!allowOverwrite), graveler.WithForce(swag.BoolValue(params.Force))) | ||
// Combine all set options |
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.
Redundant comment - the code is pretty clear
|
||
// BuildConditionFromParams creates a graveler.ConditionFunc from upload params. | ||
// Returns nil if no precondition is specified in the params. | ||
func BuildConditionFromParams(params apigen.UploadObjectParams) *graveler.ConditionFunc { |
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.
controller (pkg not just module) is exposed to graveler, on the other hand catalog is not exposed to apigen due to abstraction.
There's no perfect solution here but I believe this is the better one
// EntryCondition adapts an Entry-level condition function to a graveler.ConditionFunc. | ||
// It converts graveler Values to Entries before applying the condition, enabling Entry-based | ||
// validation logic (e.g., Object metadata checks) to work with graveler's conditional operations. | ||
func EntryCondition(conditionFunc func(*Entry) error) graveler.ConditionFunc { |
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 believe this should also handled the IfNoneMatch and consolidated the logic of condition handling
hasCondition := options.Condition != nil | ||
|
||
if !options.IfAbsent && !hasCondition { |
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.
hasCondition := options.Condition != nil | |
if !options.IfAbsent && !hasCondition { | |
hasCondition := options.Condition != nil | |
if !options.IfAbsent && !hasCondition { |
if currentValue == nil || currentValue.Identity == nil { | ||
return &value, nil | ||
// update stage with new value respecting the ifAbsent and condition | ||
return g.StagingManager.Update(ctx, branch.StagingToken, key, func(stagingValue *Value) (*Value, error) { |
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 whole logic should be refactored to support all conditions under the condition callback. adding explicit conditions and a condition callback is making this code messy and unreadable
Changes for Condition Support
This PR introduces infrastructure for conditional writes with preconditions:
Graveler Layer:
Catalog Layer:
Factory Layer:
Controller Layer: