Example of subtract

Example code of atomic operations in Multimaps — FoundationDB 7.1 looks inefficient.

@fdb.transactional
def multi_subtract(tr, index, value):
    v = tr[multi[index][value]]
    if v.present() and struct.unpack('<q', str(v))[0] > 1:
        tr.add(multi[index][value], struct.pack('<q', -1))
    else:
        del tr[multi[index][value]]

Should use compare_and_clear (recommended in the developer guide).

@fdb.transactional
def multi_subtract(tr, index, value):
    tr.add(multi[index][value], struct.pack('<q', -1))
    tr.compare_and_clear(multi[index][value], struct.pack('<q', 0))

Note
If a transaction uses both an atomic operation and a strictly serializable read on the same key, the benefits of using the atomic operation (for both conflict checking and performance) are lost.
Developer Guide — FoundationDB 7.1

Another, I think the next code

@fdb.transactional
def decrement(tr, counter):
    tr.add(counter, struct.pack('<i', -1))
    tr.compare_and_clear(counter, struct.pack('<i', -1))

is safer and easier than the original code in the developer guide.

@fdb.transactional
def decrement(tr, counter):
    tr.add(counter, struct.pack('<i', -1))
    tr.compare_and_clear(counter, struct.pack('<i', 0))

Yeah, that’s probably true. I believe the multimap example (and most of the examples) were written before COMPARE_AND_CLEAR as added. I think this should probably be a GitHub issue to improve that.

I’m not sure I quite understand this part of your post. Not sure if this a copy-paste error, but I think the argument to compare_and_clear needs to be 0 instead of -1. (If it’s -1, then the key will be deleted after the key is decremented to zero, which I think should never happen, whereas we want it to be deleted precisely when it gets decremented to zero.)

It is my intended change. Clearing a value when the value is -1 enables to decrement 0 or an empty value. Due to this advance, developers don’t have to check that the count is 1 or more (decrementable). This robust decrement code would safely work with more use cases. My decrement code is as follows.

func decrement(tr fdb.Transactor, key fdb.KeyConvertible) uint32 {
	step := []byte{0xff, 0xff, 0xff, 0xff}
	value, err := tr.Transact(func(tx fdb.Transaction) (interface{}, error) {
		tx.Add(key, step)
		tx.CompareAndClear(key, step)
		return tx.Snapshot().Get(key).Get()
	})
	if err != nil {
		log.Fatalf("Unable to perform FDB transaction (%v)", err)
	}
	return bytesToUint(value.([]byte))
}

I see. I guess I’m not sure I totally follow (i.e., if you have a count which will hopefully always be non-negative, it seems like it would never get to -1), but it may depend on your desired semantics. I suppose there are versions of this where you do want to distinguish between a zero value and no value at all

Of course my version is inappropriate in that case. However, my version works without reading the current value, right? And transactions should avoid reading the current value in atomic operations. So I think replacing tr.compare_and_clear(counter, struct.pack('<i', 0)) with tr.compare_and_clear(counter, struct.pack('<i', -1)) will make no problem. Well, the current code would be easier to understand.