etcd-io / etcd-io/etcd

Comparing value in TXN on non-existing key always returns failure.

Open
#19,678 17 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area/documentation stage/triaged type/feature
Dominant language
Go
Stars
52.3k
Forks
10.5k
Avg merge
3d 3h
Merged PRs (30d)
42

Description

Bug report criteria
  • This bug report is not security related, security issues should be disclosed privately via etcd maintainers.
  • This is not a support request or question, support requests or questions should be raised in the etcd discussion forums.
  • You have read the etcd bug reporting guidelines.
  • Existing open issues along with etcd frequently asked questions have been checked and this is not a duplicate.
What happened?

Etcd has a .Succeeded field on a Txn response to know which branch of the transaction (then or else) was run. This is critically important to know what happened.

If you have an OpTxn then the child .Succeeded values are NOT SET and we seem to always see the false zero value.

A full code reproducer is included below.

What did you expect to happen?

At least one of the two branches should see a true. Neither does.

How can we reproduce it (as minimally and precisely as possible)?

Run etcd plainly in a terminal. Then run this code:
I left in my hacking mess for anyone curious. At least one of the "SUCCESS" lines should print true. They always print false.

// etcdbug

package main

import (
	"context"
	"fmt"

	pb "go.etcd.io/etcd/api/v3/etcdserverpb"
	clientv3 "go.etcd.io/etcd/client/v3"
)

func main() {

	client, err := clientv3.New(clientv3.Config{
		Endpoints: []string{"localhost:2379"},
	})
	if err != nil {
		panic(err)
	}
	defer client.Close()

	ops := []clientv3.Op{}
	{
		ifs := []clientv3.Cmp{}
		thn := []clientv3.Op{}
		els := []clientv3.Op{}

		path := "/foo"
		data := "hello"
		ifs = append(ifs, clientv3.Compare(clientv3.Value(path), "!=", data)) // THIS ONE IS !=
		els = append(els, clientv3.OpPut(path, data))

		op := clientv3.OpTxn(ifs, thn, els)
		ops = append(ops, op)
	}
	{
		ifs := []clientv3.Cmp{}
		thn := []clientv3.Op{}
		els := []clientv3.Op{}

		path := "/bar"
		data := "world"
		ifs = append(ifs, clientv3.Compare(clientv3.Value(path), "=", data)) // THIS ONE IS =
		els = append(els, clientv3.OpPut(path, data))

		op := clientv3.OpTxn(ifs, thn, els)
		ops = append(ops, op)
	}

	txn := client.Txn(context.Background())
	txnResp, err := txn.If().Then(ops...).Else().Commit()
	if err != nil {
		panic(err)
	}

	//
	// Who even knows the correct way to pull out nested data from the
	// protobuf mess? Various approaches have been tried, thoughts welcome.
	//

	realOut := txnResp.OpResponse().Txn()

	fmt.Printf("XXX: OUT(%T): %+v\n", realOut, realOut)
	if !realOut.Succeeded {
		// else branch happened
		panic("unexpected branch")
	}

	//responses := realOut.GetResponses() // doesn't work
	responses := realOut.Responses

	fmt.Printf("XXX: NUMBER OF RESPONSES: %+v\n", len(responses))

	for _, resp := range responses {
		fmt.Printf("XXX: RESP(%T): %+v\n", resp, resp)

		x := resp.GetResponse()
		fmt.Printf("X (%T) %+v\n", x, x)

		thing, ok := x.(*pb.ResponseOp_ResponseTxn)
		if !ok {
			panic("woops")
		}

		//x := pb.ResponseOp_ResponseTxn(resp)
		fmt.Printf("THING (%T) %+v\n", thing, thing)

		// eventually we should see `true` in one of the loop iterations
		fmt.Printf("SUCCESS1? %+v\n", thing.ResponseTxn.GetSucceeded())
		fmt.Printf("SUCCESS2? %+v\n", resp.GetResponseTxn().Succeeded)

		fmt.Printf("---------------------------------\n\n")
	}
}
Anything else we need to know?

No response

Etcd version (please run commands below)
$ etcd --version
# paste output here

$ etcdctl version
# paste output here
Etcd configuration (command line flags or environment variables)

paste your configuration here

Etcd debug information (please run commands below, feel free to obfuscate the IP address or FQDN in the output)
$ etcdctl member list -w table
# paste output here

$ etcdctl --endpoints=<member list> endpoint status -w table
# paste output here
Relevant log output

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running the provided Go reproducer against a plain etcd instance and inspect the client transaction response types shown in the example, especially OpResponse().Txn() and nested ResponseTxn values. Trace how nested transaction responses are populated and verify that the child Succeeded field reflects the selected then or else branch. Done means the reproducer reports the correct branch result for both comparisons and relevant tests pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
api, database, distributed-systems
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.