Skip to content

Commit

Permalink
eval-intro.md: Delete old verbatim replication valkey-io#102
Browse files Browse the repository at this point in the history
Signed-off-by: Viktor Söderqvist <[email protected]>
  • Loading branch information
zuiderkwast committed Dec 9, 2024
1 parent 5e3b5fa commit e59a9df
Showing 1 changed file with 8 additions and 166 deletions.
174 changes: 8 additions & 166 deletions topics/eval-intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ In this case, the application should first load it with `SCRIPT LOAD` and then c
Most of [Valkey' clients](../clients/) already provide utility APIs for doing that automatically.
Please consult your client's documentation regarding the specific details.

### `!EVALSHA` in the context of pipelining
### `EVALSHA` in the context of pipelining

Special care should be given executing `EVALSHA` in the context of a [pipelined request](pipelining.md).
The commands in a pipelined request run in the order they are sent, but other clients' commands may be interleaved for execution between these.
Expand All @@ -202,7 +202,7 @@ However, from the point of view of the Valkey client, there are only two ways to
Practically speaking, it is much simpler for the client to assume that in the context of a given connection, cached scripts are guaranteed to be there unless the administrator explicitly invoked the `SCRIPT FLUSH` command.
The fact that the user can count on Valkey to retain cached scripts is semantically helpful in the context of pipelining.

## The `!SCRIPT` command
## The `SCRIPT` command

The Valkey `SCRIPT` provides several ways for controlling the scripting subsystem.
These are:
Expand All @@ -228,175 +228,17 @@ These are:

## Script replication

In standalone deployments, a single Valkey instance called _primary_ manages the entire database.
A [clustered deployment](cluster-tutorial.md) has at least three primaries managing the sharded database.
Valkey uses [replication](replication.md) to maintain one or more replicas, or exact copies, for any given primary.
In a primary-replica setup (see [replication](replication.md)), write commands performed by a script on the primary are also sent to replicas to maintain consistency.
When the script execution finishes, the sequence of commands that the script generated are wrapped into a [`MULTI`/`EXEC` transaction](transactions.md) and are sent to the replicas and written to the AOF file, if an AOF file is used. (See [Persistence](persistence.md).)
This is called *effects replication*.

Because scripts can modify the data, Valkey ensures all write operations performed by a script are also sent to replicas to maintain consistency.
There are two conceptual approaches when it comes to script replication:
In the past, it was also possible to use *verbatim replication* which means that a script was replicated as a whole, but this was removed in 7.0.

1. Verbatim replication: the primary sends the script's source code to the replicas.
Replicas then execute the script and apply the write effects.
This mode can save on replication bandwidth in cases where short scripts generate many commands (for example, a _for_ loop).
However, this replication mode means that replicas redo the same work done by the primary, which is wasteful.
More importantly, it also requires [all write scripts to be deterministic](#scripts-with-deterministic-writes).
1. Effects replication: only the script's data-modifying commands are replicated.
Replicas then run the commands without executing any scripts.
While potentially lengthier in terms of network traffic, this replication mode is deterministic by definition and therefore doesn't require special consideration.

Verbatim script replication was the only mode supported until Redis OSS 3.2, in which effects replication was added.
The _lua-replicate-commands_ configuration directive and [`server.replicate_commands()`](lua-api.md#server.replicate_commands) Lua API can be used to enable it.

In Redis OSS 5.0, effects replication became the default mode.
As of Redis OSS 7.0, verbatim replication is no longer supported.

### Replicating commands instead of scripts

Starting with Redis OSS 3.2, it is possible to select an alternative replication method.
Instead of replicating whole scripts, we can replicate the write commands generated by the script.
We call this **script effects replication**.

**Note:**
starting with Redis OSS 5.0, script effects replication is the default mode and does not need to be explicitly enabled.

In this replication mode, while Lua scripts are executed, Valkey collects all the commands executed by the Lua scripting engine that actually modify the dataset.
When the script execution finishes, the sequence of commands that the script generated are wrapped into a [`MULTI`/`EXEC` transaction](transactions.md) and are sent to the replicas and AOF.

This is useful in several ways depending on the use case:

* When the script is slow to compute, but the effects can be summarized by a few write commands, it is a shame to re-compute the script on the replicas or when reloading the AOF.
In this case, it is much better to replicate just the effects of the script.
* When script effects replication is enabled, the restrictions on non-deterministic functions are removed.
You can, for example, use the `TIME` or `SRANDMEMBER` commands inside your scripts freely at any place.
* The Lua PRNG in this mode is seeded randomly on every call.

Unless already enabled by the server's configuration or defaults (before Redis OSS 7.0), you need to issue the following Lua command before the script performs a write:

```lua
server.replicate_commands()
```

The [`server.replicate_commands()`](lua-api.md#server.replicate_commands) function returns _true) if script effects replication was enabled;
otherwise, if the function was called after the script already called a write command,
it returns _false_, and normal whole script replication is used.

This function is deprecated as of Redis OSS 7.0, and while you can still call it, it will always succeed.

### Scripts with deterministic writes

**Note:**
Starting with Redis OSS 5.0, script replication is by default effect-based rather than verbatim.
In Redis OSS 7.0, verbatim script replication had been removed entirely.
The following section only applies to versions lower than Redis OSS 7.0 when not using effect-based script replication.

An important part of scripting is writing scripts that only change the database in a deterministic way.
Scripts executed in a Valkey instance are, by default until version 5.0, propagated to replicas and to the AOF file by sending the script itself -- not the resulting commands.
Since the script will be re-run on the remote host (or when reloading the AOF file), its changes to the database must be reproducible.

The reason for sending the script is that it is often much faster than sending the multiple commands that the script generates.
If the client is sending many scripts to the primary, converting the scripts into individual commands for the replica / AOF would result in too much bandwidth for the replication link or the Append Only File (and also too much CPU since dispatching a command received via the network is a lot more work for Valkey compared to dispatching a command invoked by Lua scripts).

Normally replicating scripts instead of the effects of the scripts makes sense, however not in all the cases.
So starting with Redis OSS 3.2, the scripting engine is able to, alternatively, replicate the sequence of write commands resulting from the script execution, instead of replication the script itself.

In this section, we'll assume that scripts are replicated verbatim by sending the whole script.
Let's call this replication mode **verbatim scripts replication**.

The main drawback with the _whole scripts replication_ approach is that scripts are required to have the following property:
the script **always must** execute the same Valkey _write_ commands with the same arguments given the same input data set.
Operations performed by the script can't depend on any hidden (non-explicit) information or state that may change as the script execution proceeds or between different executions of the script.
Nor can it depend on any external input from I/O devices.

Acts such as using the system time, calling Valkey commands that return random values (e.g., `RANDOMKEY`), or using Lua's random number generator, could result in scripts that will not evaluate consistently.

To enforce the deterministic behavior of scripts, Valkey does the following:

* Lua does not export commands to access the system time or other external states.
* Valkey will block the script with an error if a script calls a Valkey command able to alter the data set **after** a Valkey _random_ command like `RANDOMKEY`, `SRANDMEMBER`, `TIME`.
That means that read-only scripts that don't modify the dataset can call those commands.
Note that a _random command_ does not necessarily mean a command that uses random numbers: any non-deterministic command is considered as a random command (the best example in this regard is the `TIME` command).
* In Redis OSS version 4.0, commands that may return elements in random order, such as `SMEMBERS` (because Sets are _unordered_), exhibit a different behavior when called from Lua,
and undergo a silent lexicographical sorting filter before returning data to Lua scripts.
So `server.call("SMEMBERS",KEYS[1])` will always return the Set elements in the same order, while the same command invoked by normal clients may return different results even if the key contains exactly the same elements.
However, starting with Redis OSS 5.0, this ordering is no longer performed because replicating effects circumvents this type of non-determinism.
In general, even when developing for Redis OSS 4.0, never assume that certain commands in Lua will be ordered, but instead rely on the documentation of the original command you call to see the properties it provides.
* Lua's pseudo-random number generation function `math.random` is modified and always uses the same seed for every execution.
This means that calling [`math.random`](lua-api.md#runtime-libraries) will always generate the same sequence of numbers every time a script is executed (unless `math.randomseed` is used).

All that said, you can still use commands that write and random behavior with a simple trick.
Imagine that you want to write a Valkey script that will populate a list with N random integers.

The initial implementation in Ruby could look like this:

```
require 'rubygems'
require 'redis'
r = Redis.new
RandomPushScript = <<EOF
local i = tonumber(ARGV[1])
local res
while (i > 0) do
res = server.call('LPUSH',KEYS[1],math.random())
i = i-1
end
return res
EOF
r.del(:mylist)
puts r.eval(RandomPushScript,[:mylist],[10,rand(2**32)])
```

Every time this code runs, the resulting list will have exactly the
following elements:

```
127.0.0.1:6379> LRANGE mylist 0 -1
1) "0.74509509873814"
2) "0.87390407681181"
3) "0.36876626981831"
4) "0.6921941534114"
5) "0.7857992587545"
6) "0.57730350670279"
7) "0.87046522734243"
8) "0.09637165539729"
9) "0.74990198051087"
10) "0.17082803611217"
```

To make the script both deterministic and still have it produce different random elements,
we can add an extra argument to the script that's the seed to Lua's pseudo-random number generator.
The new script is as follows:

```
RandomPushScript = <<EOF
local i = tonumber(ARGV[1])
local res
math.randomseed(tonumber(ARGV[2]))
while (i > 0) do
res = server.call('LPUSH',KEYS[1],math.random())
i = i-1
end
return res
EOF
r.del(:mylist)
puts r.eval(RandomPushScript,1,:mylist,10,rand(2**32))
```

What we are doing here is sending the seed of the PRNG as one of the arguments.
The script output will always be the same given the same arguments (our requirement) but we are changing one of the arguments at every invocation,
generating the random seed client-side.
The seed will be propagated as one of the arguments both in the replication link and in the Append Only File,
guaranteeing that the same changes will be generated when the AOF is reloaded or when the replica processes the script.

Note: an important part of this behavior is that the PRNG that Valkey implements as `math.random` and `math.randomseed` is guaranteed to have the same output regardless of the architecture of the system running Valkey.
32-bit, 64-bit, big-endian and little-endian systems will all produce the same output.
The [`server.replicate_commands()`](lua-api.md#server.replicate_commands) function is deprecated and has no effect, but it exists to avoid breaking existing scripts.

## Debugging Eval scripts

Starting with Redis OSS 3.2, Valkey has support for native Lua debugging.
Valkey has a built-in Lua debugger.
The Valkey Lua debugger is a remote debugger consisting of a server, which is Valkey itself, and a client, which is by default [`valkey-cli`](cli.md).

The Lua debugger is described in the [Lua scripts debugging](ldb.md) section of the Valkey documentation.
Expand Down

0 comments on commit e59a9df

Please sign in to comment.