Skip to content

Conversation

guy-har
Copy link
Contributor

@guy-har guy-har commented Oct 5, 2025

Changes for Condition Support

This PR introduces infrastructure for conditional writes with preconditions:

Graveler Layer:

  • Added ConditionFunc type for validating values before write operations
  • Added WithCondition(ConditionFunc) helper to integrate conditions into Set operations
  • The Set method now validates conditions before committing changes

Catalog Layer:

  • Added EntryCondition adapter that converts Entry-level conditions to graveler.ConditionFunc
  • Enables Entry-based validation logic (e.g., ETag checks) to work with graveler's conditional operations

Factory Layer:

  • Added BuildConditionFromParams(apigen.UploadObjectParams) function that builds conditions from API parameters
  • Currently returns nil (no-op) but provides extension point

Controller Layer:

  • Updated upload handlers to use factory.BuildConditionFromParams
  • Applies conditions via graveler.WithCondition when present

Copy link

github-actions bot commented Oct 5, 2025

📚 Documentation preview at https://pr-9566.docs-lakefs-preview.io/

(Updated: 10/8/2025, 1:52:05 PM - Commit: cc7ea2b)

@guy-har guy-har marked this pull request as ready for review October 8, 2025 13:52
@guy-har guy-har requested review from a team and N-o-Z October 8, 2025 13:53
Copy link
Contributor

@itaigilo itaigilo left a 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"
Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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 {
Copy link
Contributor

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.

Copy link
Member

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 {
Copy link
Contributor

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.

  1. Its type is already func, so it's redundant.
  2. If you insist naming it conditionFunc, so EntryCondition should also be called EntryConditionFunc, 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
Copy link
Contributor

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 {
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

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
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// update stage with new value respecting the ifAbsent and condition
// update stage with new value respecting the IfAbsent and Condition

Copy link
Contributor

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 {
Copy link
Contributor

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.

Copy link
Member

@N-o-Z N-o-Z left a 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 {
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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 {
Copy link
Member

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 {
Copy link
Member

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

Comment on lines +1858 to +1860
hasCondition := options.Condition != nil

if !options.IfAbsent && !hasCondition {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
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) {
Copy link
Member

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

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