futriix/tests/unit/cluster/scripting.tcl
zhaozhao.zz bb2b6e2927
fix scripts access wrong slot if they disagree with pre-declared keys (#12906)
Regarding how to obtain the hash slot of a key, there is an optimization
in `getKeySlot()`, it is used to avoid redundant hash calculations for
keys: when the current client is in the process of executing a command,
it can directly use the slot of the current client because the slot to
access has already been calculated in advance in `processCommand()`.

However, scripts are a special case where, in default mode or with
`allow-cross-slot-keys` enabled, they are allowed to access keys beyond
the pre-declared range. This means that the keys they operate on may not
belong to the slot of the pre-declared keys. Currently, when the
commands in a script are executed, the slot of the original client
(i.e., the current client) is not correctly updated, leading to
subsequent access to the wrong slot.

This PR fixes the above issue. When checking the cluster constraints in
a script, the slot to be accessed by the current command is set for the
original client (i.e., the current client). This ensures that
`getKeySlot()` gets the correct slot cache.

Additionally, the following modifications are made:

1. The 'sort' and 'sort_ro' commands use `getKeySlot()` instead of
`c->slot` because the client could be an engine client in a script and
can lead to potential bug.
2. `getKeySlot()` is also used in pubsub to obtain the slot for the
channel, standardizing the way slots are retrieved.
2024-01-15 09:57:12 +08:00

92 lines
3.7 KiB
Tcl

start_cluster 1 0 {tags {external:skip cluster}} {
test {Eval scripts with shebangs and functions default to no cross slots} {
# Test that scripts with shebang block cross slot operations
assert_error "ERR Script attempted to access keys that do not hash to the same slot*" {
r 0 eval {#!lua
redis.call('set', 'foo', 'bar')
redis.call('set', 'bar', 'foo')
return 'OK'
} 0}
# Test the functions by default block cross slot operations
r 0 function load REPLACE {#!lua name=crossslot
local function test_cross_slot(keys, args)
redis.call('set', 'foo', 'bar')
redis.call('set', 'bar', 'foo')
return 'OK'
end
redis.register_function('test_cross_slot', test_cross_slot)}
assert_error "ERR Script attempted to access keys that do not hash to the same slot*" {r FCALL test_cross_slot 0}
}
test {Cross slot commands are allowed by default for eval scripts and with allow-cross-slot-keys flag} {
# Old style lua scripts are allowed to access cross slot operations
r 0 eval "redis.call('set', 'foo', 'bar'); redis.call('set', 'bar', 'foo')" 0
# scripts with allow-cross-slot-keys flag are allowed
r 0 eval {#!lua flags=allow-cross-slot-keys
redis.call('set', 'foo', 'bar'); redis.call('set', 'bar', 'foo')
} 0
# Retrieve data from different slot to verify data has been stored in the correct dictionary in cluster-enabled setup
# during cross-slot operation from the above lua script.
assert_equal "bar" [r 0 get foo]
assert_equal "foo" [r 0 get bar]
r 0 del foo
r 0 del bar
# Functions with allow-cross-slot-keys flag are allowed
r 0 function load REPLACE {#!lua name=crossslot
local function test_cross_slot(keys, args)
redis.call('set', 'foo', 'bar')
redis.call('set', 'bar', 'foo')
return 'OK'
end
redis.register_function{function_name='test_cross_slot', callback=test_cross_slot, flags={ 'allow-cross-slot-keys' }}}
r FCALL test_cross_slot 0
# Retrieve data from different slot to verify data has been stored in the correct dictionary in cluster-enabled setup
# during cross-slot operation from the above lua function.
assert_equal "bar" [r 0 get foo]
assert_equal "foo" [r 0 get bar]
}
test {Cross slot commands are also blocked if they disagree with pre-declared keys} {
assert_error "ERR Script attempted to access keys that do not hash to the same slot*" {
r 0 eval {#!lua
redis.call('set', 'foo', 'bar')
return 'OK'
} 1 bar}
}
test {Cross slot commands are allowed by default if they disagree with pre-declared keys} {
r 0 flushall
r 0 eval "redis.call('set', 'foo', 'bar')" 1 bar
# Make sure the script writes to the right slot
assert_equal 1 [r 0 cluster COUNTKEYSINSLOT 12182] ;# foo slot
assert_equal 0 [r 0 cluster COUNTKEYSINSLOT 5061] ;# bar slot
}
test "Function no-cluster flag" {
R 0 function load {#!lua name=test
redis.register_function{function_name='f1', callback=function() return 'hello' end, flags={'no-cluster'}}
}
catch {R 0 fcall f1 0} e
assert_match {*Can not run script on cluster, 'no-cluster' flag is set*} $e
}
test "Script no-cluster flag" {
catch {
R 0 eval {#!lua flags=no-cluster
return 1
} 0
} e
assert_match {*Can not run script on cluster, 'no-cluster' flag is set*} $e
}
}