Skip to content

Commit 306ee73

Browse files
adam-fowlerheckj
andauthored
Transaction documentation (#156)
* Transaction docs Signed-off-by: Adam Fowler <[email protected]> * Update Sources/Valkey/Documentation.docc/Pipelining.md Co-authored-by: Joseph Heck <[email protected]> Signed-off-by: Adam Fowler <[email protected]> * Update Sources/Valkey/Documentation.docc/Pipelining.md Co-authored-by: Joseph Heck <[email protected]> Signed-off-by: Adam Fowler <[email protected]> * Update Sources/Valkey/Documentation.docc/Transactions.md Co-authored-by: Joseph Heck <[email protected]> Signed-off-by: Adam Fowler <[email protected]> * Update Sources/Valkey/Documentation.docc/Transactions.md Co-authored-by: Joseph Heck <[email protected]> Signed-off-by: Adam Fowler <[email protected]> * Edit check and set text Signed-off-by: Adam Fowler <[email protected]> * Transactions are only available on connections Signed-off-by: Adam Fowler <[email protected]> --------- Signed-off-by: Adam Fowler <[email protected]> Co-authored-by: Joseph Heck <[email protected]>
1 parent 4ede3ac commit 306ee73

File tree

3 files changed

+66
-9
lines changed

3 files changed

+66
-9
lines changed

Sources/Valkey/Documentation.docc/Pipelining.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Pipelining Commands
1+
# Pipelining
22

3-
Sending multiple commands at once without waiting for the response of each command
3+
Send multiple commands at once without waiting for the response of each command.
44

55
Valkey pipelining is a technique for improving performance by issuing multiple commands at once without waiting for the response to each individual command. Pipelining not only reduces the latency cost of waiting for the result of each command it also reduces the cost to the server as it reduces I/O costs. Multiple commands can be read with a single syscall, and multiple results are delivered with a single syscall.
66

@@ -10,9 +10,9 @@ In valkey-swift each command has its own type conforming to the protocol ``Valke
1010

1111
```swift
1212
let (_,_, getResult) = await valkeyClient.pipeline(
13-
SET(key: "foo", value: "100"),
14-
INCR(key: "foo")
15-
GET(key: "foo")
13+
SET("foo", value: "100"),
14+
INCR("foo")
15+
GET("foo")
1616
)
1717
// get returns an optional ByteBuffer
1818
if let result = try getResult.get().map({ String(buffer: $0) }) {
@@ -28,10 +28,10 @@ Being able to have multiple requests in transit on a single connection means we
2828
try await client.withConnection { connection in
2929
try await withThrowingTaskGroup(of: Void.self) { group in
3030
group.addTask {
31-
_ = try await connection.lpush(key: "fooList", element: ["bar"])
31+
_ = try await connection.lpush("fooList", elements: ["bar"])
3232
}
3333
group.addTask {
34-
_ = try await connection.rpush(key: "fooList2", element: ["baz"])
34+
_ = try await connection.rpush("fooList2", elements: ["baz"])
3535
}
3636
try await group.waitForAll()
3737
}
@@ -41,9 +41,12 @@ try await client.withConnection { connection in
4141
You can also use `async let` to run commands without waiting for their results immediately.
4242

4343
```swift
44-
async let asyncResult = connection.lpush(key: "fooList", element: ["bar"])
44+
async let asyncResult = connection.lpush("fooList", elements: ["bar"])
4545
// do something else
4646
let result = try await asyncResult
4747
```
4848

49-
Be careful when using a single connection across multiple Tasks though. The result of a command will only become available when the result of any previous command queued has been made available. So a command that either blocks the connection or takes a long time could affect the response time of commands that follow it.
49+
Be careful when using a single connection across multiple Tasks though. The result of a command only becomes available when the server makes available the result of the command previously queued. Because of this, a command that either blocks the connection or takes a long time can affect the response time of commands that follow it.
50+
51+
You can find out more about pipelining of commands in the [Valkey documentation](https://valkey.io/topics/pipelining/).
52+
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Transactions
2+
3+
Perform atomic operations.
4+
5+
Transactions allow you to group multiple commands into an atomic operation. A request sent by another client isn't processed in the middle of the execution of a transaction. Valkey uses the commands `MULTI` and `EXEC` to setup and execute a transaction. After initiating a transaction with `MULTI`, commands return a simple string `QUEUED` to indicate the server queued the commands. When the client sends the `EXEC` command, the server executes all the queued commands and returns an array with their results.
6+
7+
Because of this custom behaviour valkey-swift provides extra support for executing transactions. The API is very similar to the pipelining function which accepts a parameter pack of commands, detailed in <doc:Pipelining>.
8+
9+
```swift
10+
try await valkeyClient.withConnection { connection in
11+
let results = try await connection.transaction(
12+
SET("foo", value: "100"),
13+
LPUSH("queue", elements: ["foo"])
14+
)
15+
let lpushResponse = try results.1.get()
16+
}
17+
```
18+
19+
### Rollbacks
20+
21+
Valkey does not support rollbacks of transactions for simplicity and performance reasons.
22+
23+
### Check and set
24+
25+
The transaction command `WATCH` is used to add a check-and-set behaviour to Valkey transactions. This sets up a list of keys to WATCH, and if any changes to them are detected before the next transaction is executed on the same connection, then that transaction will fail.
26+
27+
For instance, imagine we wanted to atomically increment a counter (assuming we don't have the INCR command). A simple implementation might look like this:
28+
29+
```swift
30+
// get value, otherwise default to 0
31+
let value = try await connection.get("counter").map { Int(String(buffer: $0)) } ?? 0
32+
try await connection.set("counter", String(value + 1))
33+
```
34+
35+
Unfortunately this isn't a reliable solution as another client could attempt to increment the key "counter" in between the GET and SET commands, then the increment would be applied to the wrong value. By using WATCH and executing the SET inside a transaction we can avoid this. If the key is edited between the WATCH and SET, the transaction fails and throws a `ValkeyClientError(.transactionAborted)` error. When this occurs we know the key was edited between these two commands and we need to update the "counter" value before trying to call SET again, so we run the operation again.
36+
37+
```swift
38+
while true {
39+
try await connection.watch(keys: ["counter"])
40+
let value = try await connection.get("counter").map { Int(String(buffer: $0)) } ?? 0
41+
do {
42+
let result = try await connection.transaction(
43+
SET("counter", String(value + 1))
44+
)
45+
// set was succesful break out of the while loop
46+
break
47+
} catch let error as ValkeyClientError where error.errorCode == .transactionAborted {
48+
// Cancelled SET because "counter" was edited after WATCH, try again
49+
}
50+
}
51+
```
52+
53+
More can be found out about Valkey transactions in the [Valkey documentation](https://valkey.io/topics/transactions/).

Sources/Valkey/Documentation.docc/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ try await valkeyClient.withConnection { connection in
4545
### Articles
4646

4747
- <doc:Pipelining>
48+
- <doc:Transactions>
4849

4950
### Client
5051

0 commit comments

Comments
 (0)