Skip to content
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

Skip unnecessary writes in default impl for modifyGet #70

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions datastore/datastore.nim
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ method modifyGet*(self: Datastore, key: Key, fn: ModifyGet): Future[?!seq[byte]]
## | some(u) | none | delete u |
## | some(u) | some(v) | replace u with v (if u != v) |
##
## If `fn` raises an error, the value associated with `key` remains unchanged and raised error is wrapped
## into a failure and returned as a result.
##
## Note that `fn` can be called multiple times (when concurrent modification was detected). In such case
## only the last auxillary value is returned.
##
Expand Down
4 changes: 4 additions & 0 deletions datastore/defaultimpl.nim
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ proc defaultModifyGetImpl*(
except CatchableError as err:
return failure(err)

if maybeCurrentData == maybeNewData:
Copy link
Member

@gmega gmega Aug 15, 2024

Choose a reason for hiding this comment

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

🔴 Well technically you should be able to test for this (the absence of a write), and I'd argue you should test for it as otherwise you can't know if your optimization is actually working (which is something we care about, as otherwise there'd be no point in putting it there), no matter how convincing the code looks. 🙂

I'm wiling to let this pass - against my own best judgement - if you tell me that writing such a test is complicated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that skipping writes is not such a crucial feature that would justify having a test that verify if something was called or not called (one of the worst kinds of tests imo), but I've added such test anyway.

# no need to change currently stored value (if any)
return aux.success

if newData =? maybeNewData:
if err =? (await self.put(key, newData)).errorOption:
return failure(err)
Expand Down
2 changes: 1 addition & 1 deletion datastore/sql/sqliteds.nim
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ method modifyGet*(self: SQLiteDatastore, key: Key, fn: ModifyGet): Future[?!seq[
return failure(err)

if maybeCurrentData == maybeNewData:
# no need to change currently stored value
# no need to change currently stored value (if any)
break

if err =? self.db.beginStmt.exec().errorOption:
Expand Down
95 changes: 95 additions & 0 deletions tests/datastore/testdefaultimpl.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import std/options
import std/sequtils
import std/tables

import pkg/asynctest/chronos/unittest2
import pkg/chronos
import pkg/questionable
import pkg/questionable/results

import pkg/datastore
import pkg/datastore/defaultimpl

type
MockDatastore* = ref object of Datastore
values: TableRef[Key, seq[byte]]
lock: AsyncLock
calls: seq[MethodCall]

Method = enum
Put, Get, Delete, Has

MethodCall = object
key: Key
case kind: Method
of Put:
data: seq[byte]
of Get, Delete, Has:
discard

method put*(self: MockDatastore, key: Key, data: seq[byte]): Future[?!void] {.async.} =
self.calls.add(MethodCall(kind: Put, key: key, data: data))
self.values[key] = data
success()

method get*(self: MockDatastore, key: Key): Future[?!seq[byte]] {.async.} =
self.calls.add(MethodCall(kind: Get, key: key))
if key notin self.values:
failure(newException(DatastoreKeyNotFound, "Key doesn't exist"))
else:
success(self.values[key])

method has*(self: MockDatastore, key: Key): Future[?!bool] {.async.} =
self.calls.add(MethodCall(kind: Has, key: key))
success(key in self.values)

method delete*(self: MockDatastore, key: Key): Future[?!void] {.async.} =
self.calls.add(MethodCall(kind: Delete, key: key))
self.values.del(key)
success()

method modifyGet*(self: MockDatastore, key: Key, fn: ModifyGet): Future[?!seq[byte]] {.async.} =
await defaultModifyGetImpl(self, self.lock, key, fn)

method modify*(self: MockDatastore, key: Key, fn: Modify): Future[?!void] {.async.} =
await defaultModifyImpl(self, self.lock, key, fn)

proc new*(
T: type MockDatastore): T =
T(
values: newTable[Key, seq[byte]](),
lock: newAsyncLock(),
calls: newSeq[MethodCall]()
)

suite "Test defaultimpl":
var
ds: MockDatastore
key: Key

setup:
ds = MockDatastore.new()
key = Key.init("/a/b").tryGet()

test "should put a value that is different than the current value":

(await ds.put(key, @[byte 1, 2, 3])).tryGet()

proc modifyFn(maybeCurr: ?seq[byte]): Future[?seq[byte]] {.async.} =
some(@[byte 3, 2, 1])

(await ds.modify(key, modifyFn)).tryGet()

check:
ds.calls.filterIt(it.kind == Put).len == 2

test "should not attempt to put a value that is equal to the current value":
(await ds.put(key, @[byte 1, 2, 3])).tryGet()

proc modifyFn(maybeCurr: ?seq[byte]): Future[?seq[byte]] {.async.} =
some(@[byte 1, 2, 3])

(await ds.modify(key, modifyFn)).tryGet()

check:
ds.calls.filterIt(it.kind == Put).len == 1
3 changes: 2 additions & 1 deletion tests/testall.nim
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import
./datastore/testsql,
./datastore/testleveldb,
./datastore/testtieredds,
./datastore/testmountedds
./datastore/testmountedds,
./datastore/testdefaultimpl

{.warning[UnusedImport]: off.}