Pipelines & transactions
Pipelines and transactions both group several commands, but they serve different purposes. A pipeline improves throughput by sending many commands in one round trip, without atomicity. A transaction runs the grouped commands as a unit and can protect them from concurrent changes.
Pipelines
A pipeline sends several Command values in one round trip and returns their results as a typed tuple. The commands are not atomic, so commands from other clients may run between them. In a cluster, Sage routes each command by key and restores the original result order. Cross-slot commands work the same way inside and outside a pipeline.
client.set("pipe:a", "x")
client.set("pipe:n", 10)
val tuple = client.pipeline(
(
Commands.get[String, String]("pipe:a"),
Commands.incrBy("pipe:n", 5)
)
)
// tuple: (Option[String], Long)for {
_ <- client.set("pipe:a", "x")
_ <- client.set("pipe:n", 10)
tuple <- client.pipeline(
(
Commands.get[String, String]("pipe:a"),
Commands.incrBy("pipe:n", 5)
)
)
} yield tuple // (Option[String], Long)A tuple gives a fixed-size result whose elements can have different types. When commands are built dynamically and share a result type, pass a Seq[Command[A]] instead and get back a Vector[A] in the same order. An empty Seq returns an empty result without contacting the server:
val ids = List("a", "b", "c")
client.pipeline(ids.map(id => Commands.get[String, String](id))) // F[Vector[Option[String]]]By default, a pipeline fails as a whole if any command fails. Use pipelineAttempt to return each command's result separately:
client.set("pipe:str", "hello")
// INCR on a non-numeric string fails only at its own position;
// the GET still succeeds
val attempt = client.pipelineAttempt(
(
Commands.get[String, String]("pipe:str"),
Commands.incr("pipe:str")
)
)for {
_ <- client.set("pipe:str", "hello")
// INCR on a non-numeric string fails only at its own position;
// the GET still succeeds
attempt <- client.pipelineAttempt(
(
Commands.get[String, String]("pipe:str"),
Commands.incr("pipe:str")
)
)
} yield attemptIn a cluster, a pipeline creates a separate batch for each node. It cannot include a command that runs on every master. Sage rejects these commands before sending the pipeline; run them on the client directly.
Transactions
A transaction runs a pipeline atomically with MULTI/EXEC on a temporary dedicated connection. Open one with transaction { tx => ... }. Inside the scope, you can watch keys, run ordinary reads such as tx.get or tx.run, and call exec with a pipeline. Leaving the scope without calling exec discards the transaction.
exec returns an Option. If a watched key changed before EXEC, the transaction does not run and returns None. This is an expected result that you can retry:
client.set("tx:n", 1)
val result = client.transaction { tx =>
tx.watch("tx:n")
tx.get[Int]("tx:n")
tx.exec(
(Commands.incr("tx:n"), Commands.incrBy("tx:n", 4))
)
}
// result: Some((2, 6)), or None if "tx:n" changed before EXECfor {
_ <- client.set("tx:n", 1)
result <- client.transaction { tx =>
for {
_ <- tx.watch("tx:n")
_ <- tx.get[Int]("tx:n")
res <- tx.exec(
(
Commands.incr("tx:n"),
Commands.incrBy("tx:n", 4)
)
)
} yield res
}
} yield result // Some((2, 6)), or None if "tx:n" changedRedis transactions impose these rules:
- Reads inside the scope must be ordinary commands. Sage rejects a blocking command instead of holding the leased connection.
- A rejection while Redis queues commands discards the whole transaction. Nothing runs.
- An execution error does not roll back the other commands. Sage reports the error for that command.
- In a cluster, every key in the transaction must hash to one slot. Use a hash tag when related keys need to share a slot. Pipelines can use commands with documented cross-slot support.
- In a cluster, set a retry limit. A transaction does not follow redirects because doing so would break atomicity. Retry the whole block with backoff. While a slot is migrating, no retry can commit until the migration finishes.
Which to use
Use a pipeline when the commands are independent and you want fewer round trips. Use a transaction when the commands must run as a unit or when you need WATCH to protect them from concurrent changes.