Lua Scripting
This guide covers how to use Lua scripts with Valkey GLIDE for Python, including the Script class, script execution, management, and best practices.
Table of Contents
Section titled “Table of Contents”- Prerequisites and Setup
- Overview
- Basic Script Usage
- Scripts with Keys and Arguments
- Script Management
- Cluster Mode Considerations
- Advanced Features
- Best Practices
- Error Handling
- Migration from Direct EVAL
Prerequisites and Setup
Section titled “Prerequisites and Setup”Required Imports
Section titled “Required Imports”import asynciofrom glide import ( Script, GlideClient, GlideClientConfiguration, NodeAddress)
# For script management examplesfrom glide import FlushMode
# For error handling examplesfrom glide import RequestError
# For cluster examplesfrom glide import GlideClusterClient, GlideClusterClientConfiguration, SlotKeyRoute, SlotType, AllPrimariesBasic Client Setup
Section titled “Basic Client Setup”# Standalone client configurationconfig = GlideClientConfiguration(addresses=[NodeAddress("localhost", 6379)])
# Create clientclient = await GlideClient.create(config)
# Always close the client when doneawait client.close()Cluster Client Setup (for cluster examples)
Section titled “Cluster Client Setup (for cluster examples)”# Cluster client configurationconfig = GlideClusterClientConfiguration(addresses=[NodeAddress("localhost", 7000)])
# Create cluster clientcluster_client = await GlideClusterClient.create(config)
# Always close when doneawait cluster_client.close()Requirements
Section titled “Requirements”- Running Valkey/Redis server on localhost:6379 (or cluster on ports 7000+)
- GLIDE Python client installed
- Python 3.8+ with asyncio support
Running the Examples
Section titled “Running the Examples”All examples assume you have an async context. For standalone scripts, use:
async def main(): # Your example code here pass
if __name__ == "__main__": asyncio.run(main())Important Notes:
- All script arguments must be strings, not integers
- Keys and arguments are automatically encoded by GLIDE
- Scripts are cached automatically using SHA1 hashes
Overview
Section titled “Overview”Valkey GLIDE provides a Script class that wraps Lua scripts and handles their execution efficiently. Scripts are automatically cached using SHA1 hashes and executed via EVALSHA for optimal performance.
Key Benefits
Section titled “Key Benefits”- Automatic Caching: Scripts are cached using SHA1 hashes for efficient reuse
- Cluster Support: Scripts work seamlessly in both standalone and cluster modes
- Performance: Uses
EVALSHAinternally to avoid sending script code repeatedly - Management: Built-in methods for script lifecycle management
Basic Script Usage
Section titled “Basic Script Usage”Creating and Executing Simple Scripts
Section titled “Creating and Executing Simple Scripts”from glide import Script, GlideClient, GlideClientConfiguration, NodeAddress
# Create a clientconfig = GlideClientConfiguration(addresses=[NodeAddress("localhost", 6379)])client = await GlideClient.create(config)
# Create a simple scriptscript = Script("return 'Hello, Valkey!'")
# Execute the scriptresult = await client.invoke_script(script)print(result) # b'Hello, Valkey!'Scripts with Return Values
Section titled “Scripts with Return Values”# Script that returns a numberscript = Script("return 42")result = await client.invoke_script(script)print(result) # 42
# Script that returns an arrayscript = Script("return {1, 2, 3, 'hello'}")result = await client.invoke_script(script)print(result) # [1, 2, 3, b'hello']Scripts with Keys and Arguments
Section titled “Scripts with Keys and Arguments”Scripts can access keys and arguments through the KEYS and ARGV arrays.
Using KEYS Array
Section titled “Using KEYS Array”# Script that operates on keysscript = Script("return redis.call('GET', KEYS[1])")
# Execute with keysresult = await client.invoke_script(script, keys=["mykey"])Using ARGV Array
Section titled “Using ARGV Array”# Script that uses argumentsscript = Script("return 'Hello, ' .. ARGV[1]")
# Execute with argumentsresult = await client.invoke_script(script, args=["World"])print(result) # b'Hello, World'Combining Keys and Arguments
Section titled “Combining Keys and Arguments”# Script that sets a key-value pairscript = Script("return redis.call('SET', KEYS[1], ARGV[1])")
# Execute with both keys and argumentsresult = await client.invoke_script( script, keys=["user:1000:name"], args=["John Doe"])print(result) # b'OK'
# Script that gets and modifies a valuescript = Script(""" local current = redis.call('GET', KEYS[1]) if current then return redis.call('SET', KEYS[1], current .. ARGV[1]) else return redis.call('SET', KEYS[1], ARGV[1]) end""")
result = await client.invoke_script( script, keys=["counter"], args=[":increment"])Working with Multiple Keys
Section titled “Working with Multiple Keys”# Script that works with multiple keysscript = Script(""" local key1_val = redis.call('GET', KEYS[1]) local key2_val = redis.call('GET', KEYS[2]) return {key1_val, key2_val}""")
result = await client.invoke_script( script, keys=["key1", "key2"])Script Management
Section titled “Script Management”Script Hashing and Caching
Section titled “Script Hashing and Caching”Each script is automatically assigned a SHA1 hash for efficient caching:
script = Script("return 'Hello'")
# Get the script's SHA1 hashhash_value = script.get_hash()print(f"Script hash: {hash_value}")
# Scripts with the same code have the same hashscript2 = Script("return 'Hello'")assert script.get_hash() == script2.get_hash()Viewing Script Source (Valkey 8.0+)
Section titled “Viewing Script Source (Valkey 8.0+)”# Load a scriptscript = Script("return 'Hello World'")await client.invoke_script(script)
# Show the original source codesource = await client.script_show(script.get_hash())print(source) # b"return 'Hello World'"Checking Script Existence
Section titled “Checking Script Existence”# Check if scripts exist in the server cachescript1 = Script("return 'Script 1'")script2 = Script("return 'Script 2'")
# Load script1 by executing itawait client.invoke_script(script1)
# Check existence of both scriptsexists = await client.script_exists([ script1.get_hash(), script2.get_hash()])print(exists) # [True, False] - only script1 was loadedFlushing Script Cache
Section titled “Flushing Script Cache”# Load a scriptscript = Script("return 'Test'")await client.invoke_script(script)
# Verify it existsexists = await client.script_exists([script.get_hash()])print(exists) # [True]
# Flush the script cache on the serverawait client.script_flush()
# Verify script is gone from serverexists = await client.script_exists([script.get_hash()])print(exists) # [False]
# Flush with ASYNC mode (non-blocking)await client.script_flush(FlushMode.ASYNC)
# Verify script still exist from clientprint(f"Can still access hash: {script.get_hash()}")
# Explicit cleanup of client-side Script objectdel scriptKilling Running Scripts
Section titled “Killing Running Scripts”A script can be safely killed if it has only performed read-only operations. However, once it executes any write operation, it becomes uninterruptible and must either run to completion or reach a timeout.
# This long-running script CAN be killed (read-only)killable_long_script = Script(""" local start = redis.call('TIME')[1] while redis.call('TIME')[1] - start < 10 do redis.call('GET', 'some_key') -- Read-only operations end return 'Done'""")
# This long-running script CANNOT be killed (performs writes)unkillable_script = Script(""" redis.call('SET', 'temp', 'value') -- Write operation local start = redis.call('TIME')[1] while redis.call('TIME')[1] - start < 10 do -- Long operation after write end return 'Done'""")
# In one task, run the scriptasync def run_script(): try: await client.invoke_script(killable_long_script) except RequestError as e: if "Script killed" in str(e): print("Script was killed")
# In another task, kill the scriptasync def kill_script(): await asyncio.sleep(2) # Wait a bit await client.script_kill() print("Script killed")
# Run both tasks concurrentlyawait asyncio.gather(run_script(), kill_script())Cluster Mode Considerations
Section titled “Cluster Mode Considerations”Default Behavior
Section titled “Default Behavior”In cluster mode, scripts are automatically routed to the appropriate nodes:
from glide import GlideClusterClient
cluster_client = await GlideClusterClient.create(config)
# This works the same in cluster modescript = Script("return redis.call('SET', KEYS[1], ARGV[1])")result = await cluster_client.invoke_script( script, keys=["user:1000"], # Routed based on key hash args=["John"])Explicit Routing
Section titled “Explicit Routing”You can explicitly route scripts to specific nodes:
from glide import SlotKeyRoute, SlotType, AllPrimaries
# Route to a specific slotroute = SlotKeyRoute(SlotType.PRIMARY, "user:1000")result = await cluster_client.invoke_script_route( script, keys=["user:1000"], args=["John"], route=route)
# Route to all primary nodesroute = AllPrimaries()result = await cluster_client.invoke_script_route( script, route=route)Multi-Slot Scripts
Section titled “Multi-Slot Scripts”Scripts that access multiple keys must ensure all keys belong to the same slot in cluster mode:
# This WILL ALWAYS FAIL in cluster mode if keys are in different slots# The server rejects the request immediately with CROSSSLOT errorscript = Script(""" redis.call('SET', KEYS[1], ARGV[1]) redis.call('SET', KEYS[2], ARGV[2]) return 'OK'""")
# This will be rejected before executiontry: result = await cluster_client.invoke_script( script, keys=["key1", "key2"], # Different slots - ALWAYS fails args=["value1", "value2"] )except RequestError as e: if "CrossSlot" in str(e): print("Keys are in different slots - script rejected")
# Keys must be in the same slot using hash tagsresult = await cluster_client.invoke_script( script, keys=["user:{1000}:name", "user:{1000}:email"], # Same slot due to {1000} args=["John", "john@example.com"])Advanced Features
Section titled “Advanced Features”Binary Data Support
Section titled “Binary Data Support”Scripts can work with binary data:
# Script with binary inputscript = Script(bytes("return ARGV[1]", "utf-8"))
# Execute with binary argumentsresult = await client.invoke_script( script, args=[bytes("binary data", "utf-8")])Large Keys and Arguments
Section titled “Large Keys and Arguments”GLIDE handles large keys and arguments efficiently:
# Large key (8KB)large_key = "0" * (2**13)script = Script("return KEYS[1]")result = await client.invoke_script(script, keys=[large_key])
# Large arguments (4KB each)large_arg1 = "0" * (2**12)large_arg2 = "1" * (2**12)script = Script("return ARGV[2]")result = await client.invoke_script(script, args=[large_arg1, large_arg2])Script Reuse and Performance
Section titled “Script Reuse and Performance”Scripts are automatically cached and reused:
# Create a reusable scriptincrement_script = Script(""" local current = redis.call('GET', KEYS[1]) if current then return redis.call('SET', KEYS[1], current + ARGV[1]) else return redis.call('SET', KEYS[1], ARGV[1]) end""")
# Use the same script multiple times - efficient due to cachingfor i in range(100): await client.invoke_script( increment_script, keys=[f"counter:{i}"], args=["1"] )Best Practices
Section titled “Best Practices”1. Use Scripts for Atomic Non-primitive Operations
Section titled “1. Use Scripts for Atomic Non-primitive Operations”# Good: Conditional update with multiple data structuresconditional_update = Script(""" local current = redis.call('GET', KEYS[1]) local threshold = tonumber(ARGV[2])
if current and tonumber(current) >= threshold then redis.call('SET', KEYS[1], ARGV[1]) redis.call('LPUSH', KEYS[2], ARGV[1]) redis.call('EXPIRE', KEYS[2], ARGV[3]) return 1 else return 0 end""")
result = await client.invoke_script( conditional_update, keys=["user:score", "user:history"], args=["100", "50", "86400"] # new score, threshold, expire in 1 day)2. Handle Nil Values Properly
Section titled “2. Handle Nil Values Properly”# Good: Proper nil handlingsafe_script = Script(""" local val = redis.call('GET', KEYS[1]) if val then return val else return 'default_value' end""")3. Use Appropriate Data Types
Section titled “3. Use Appropriate Data Types”# Good: Return appropriate typestyped_script = Script(""" local value = redis.call('GET', KEYS[1]) return tonumber(value) or 0 -- Ensure numeric return, default to 0 if nil""")4. Consider Cluster Constraints
Section titled “4. Consider Cluster Constraints”# Good: Use hash tags for related keyscluster_script = Script(""" redis.call('SET', KEYS[1], ARGV[1]) redis.call('SET', KEYS[2], ARGV[2]) return 'OK'""")
# Execute with hash tagsawait cluster_client.invoke_script( cluster_script, keys=["user:{123}:name", "user:{123}:email"], args=["John", "john@example.com"])Error Handling
Section titled “Error Handling”Common Script Errors
Section titled “Common Script Errors”import asynciofrom glide import RequestError
# Handle script execution errorsscript = Script("return redis.call('INCR', 'not_a_number')")
try: result = await client.invoke_script(script)except RequestError as e: if "WRONGTYPE" in str(e) or "not an integer" in str(e): print("Type error in script") elif "syntax error" in str(e).lower(): print("Lua syntax error in script") elif "unknown command" in str(e).lower(): print("Invalid Redis command in script") else: print(f"Script error: {e}")Script Timeout Handling
Section titled “Script Timeout Handling”# Configure client timeout for long-running scriptsconfig = GlideClientConfiguration( addresses=[NodeAddress("localhost", 6379)], request_timeout=30000 # 30 seconds for long scripts (default is usually 5000ms))client = await GlideClient.create(config)
# Handle long-running scriptslong_script = Script(""" local start = redis.call('TIME')[1] while redis.call('TIME')[1] - start < 25 do redis.call('GET', 'dummy_key') -- Read-only operation end return 'Done'""")
try: result = await client.invoke_script(long_script) print(f"Script completed: {result.decode('utf-8')}")except RequestError as e: if "timeout" in str(e).lower(): print("Client timeout - script may still be running on server!") print("Consider increasing request_timeout in client configuration") elif "Script killed" in str(e): print("Script was killed by server (only possible for read-only scripts)") else: print(f"Script error: {e}")
# Important: Client timeout != Script termination# - Client stops waiting for response# - Script continues running on server# - Use SCRIPT KILL to stop read-only scripts if neededCluster-Specific Errors
Section titled “Cluster-Specific Errors”# Handle cluster routing errorstry: result = await cluster_client.invoke_script( script, keys=["key1", "key2"] # Might be in different slots )except RequestError as e: if "CROSSSLOT" in str(e): print("Keys are in different slots") # Use hash tags or route explicitlyBatch Operations and Transactions
Section titled “Batch Operations and Transactions”Currently, invoke_script is not supported in batch operations (pipelines/transactions). To use Lua scripts within an atomic batch (MULTI/EXEC transaction), you must use the EVAL command with custom_command.
Note: The Transaction class is deprecated. Use Batch(is_atomic=True) instead.
Using EVAL in Atomic Batches
Section titled “Using EVAL in Atomic Batches”from glide import Batch
# Create an atomic batch (transaction)batch = Batch(is_atomic=True)batch.set("batch-key", "batch-value")batch.get("batch-key")batch.custom_command(["EVAL", "return 'Hello from Lua!'", "0"])
# Execute the batchresults = await client.exec(batch=batch, raise_on_error=False)
print("Batch executed:")print(f"SET result: {results[0]}") # b'OK'print(f"GET result: {results[1]}") # b'batch-value'print(f"EVAL result: {results[2]}") # b'Hello from Lua!'EVAL with Keys and Arguments in Atomic Batches
Section titled “EVAL with Keys and Arguments in Atomic Batches”# Script with keys and argumentsbatch = Batch(is_atomic=True)batch.custom_command([ "EVAL", "return redis.call('SET', KEYS[1], ARGV[1])", "1", # Number of keys "script-key", # Key "script-value" # Argument])batch.get("script-key")
results = await client.exec(batch, raise_on_error=False)print(f"EVAL result: {results[0]}") # b'OK'print(f"GET result: {results[1]}") # b'script-value'Migration from Direct EVAL
Section titled “Migration from Direct EVAL”If you’re migrating from direct EVAL commands, here’s how to adapt:
Before (Direct EVAL)
Section titled “Before (Direct EVAL)”# Old approach with custom commands (not recommended)result = await client.custom_command([ "EVAL", "return redis.call('SET', KEYS[1], ARGV[1])", "1", "mykey", "myvalue"])After (Script Class)
Section titled “After (Script Class)”# New approach with Script class (recommended)script = Script("return redis.call('SET', KEYS[1], ARGV[1])")result = await client.invoke_script( script, keys=["mykey"], args=["myvalue"])Benefits of Migration
Section titled “Benefits of Migration”- Automatic Caching: Scripts are cached automatically
- Better Error Handling: More specific error types
- Cluster Support: Automatic routing in cluster mode
- Type Safety: Better integration with GLIDE’s type system
- Performance: Optimized execution path
Examples Repository
Section titled “Examples Repository”Here are some common script patterns:
Rate Limiting
Section titled “Rate Limiting”rate_limit_script = Script(""" local key = KEYS[1] local limit = tonumber(ARGV[1]) local window = tonumber(ARGV[2])
local current = redis.call('GET', key) if current == false then redis.call('SET', key, 1) redis.call('EXPIRE', key, window) return {1, limit} end
current = tonumber(current) if current < limit then local new_val = redis.call('INCR', key) local ttl = redis.call('TTL', key) return {new_val, limit} else local ttl = redis.call('TTL', key) return {current, limit, ttl} end""")
# Usageresult = await client.invoke_script( rate_limit_script, keys=["rate_limit:user:123"], args=["10", "60"] # 10 requests per 60 seconds)Distributed Lock
Section titled “Distributed Lock”acquire_lock_script = Script(""" if redis.call('SET', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2]) then return 1 else return 0 end""")
release_lock_script = Script(""" if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end""")
# Acquire locklock_acquired = await client.invoke_script( acquire_lock_script, keys=["lock:resource:123"], args=["unique_token", "30"] # 30 second expiration)
if lock_acquired: try: # Do work while holding lock pass finally: # Release lock await client.invoke_script( release_lock_script, keys=["lock:resource:123"], args=["unique_token"] )Conditional Update
Section titled “Conditional Update”conditional_update_script = Script(""" local current = redis.call('GET', KEYS[1]) if current == ARGV[1] then redis.call('SET', KEYS[1], ARGV[2]) return 1 else return 0 end""")
# Update only if current value matches expectedupdated = await client.invoke_script( conditional_update_script, keys=["user:123:status"], args=["pending", "active"] # Change from "pending" to "active")Conclusion
Section titled “Conclusion”Valkey GLIDE’s Script class provides a powerful and efficient way to execute Lua scripts. By following the patterns and best practices outlined in this guide, you can:
- Write efficient, atomic operations
- Handle complex business logic server-side
- Ensure optimal performance through automatic caching
- Work seamlessly in both standalone and cluster environments
For more information, see the Valkey Lua scripting documentation and the GLIDE API documentation.