docs
0
# Lua Scripting
1
2
Redis Lua scripting support with script caching and execution. Lua scripts provide atomic operations, server-side computation, and complex logic execution with full access to Redis data structures and commands, enabling powerful custom operations that maintain data consistency.
3
4
## Capabilities
5
6
### Script Execution
7
8
Core functions for executing Lua scripts with Redis data access.
9
10
```python { .api }
11
def eval(
12
self,
13
script: str,
14
numkeys: int,
15
*keys_and_args: KeyT
16
) -> Any: ...
17
18
def evalsha(
19
self,
20
sha: str,
21
numkeys: int,
22
*keys_and_args: KeyT
23
) -> Any: ...
24
```
25
26
### Script Management
27
28
Functions for managing script cache and script lifecycle.
29
30
```python { .api }
31
def script_load(self, script: str) -> str: ...
32
33
def script_exists(self, *args: str) -> List[bool]: ...
34
35
def script_flush(self, sync_type: Optional[str] = None) -> bool: ...
36
37
def script_kill(self) -> bool: ...
38
```
39
40
### Script Debugging
41
42
Redis 3.2+ debugging capabilities for Lua script development.
43
44
```python { .api }
45
def script_debug(self, *args) -> str: ...
46
```
47
48
## Usage Examples
49
50
### Basic Lua Scripts
51
52
```python
53
import fakeredis
54
55
client = fakeredis.FakeRedis()
56
57
# Simple script that increments a counter and returns the new value
58
increment_script = """
59
local key = KEYS[1]
60
local increment = tonumber(ARGV[1]) or 1
61
62
local current = redis.call('GET', key)
63
if current == false then
64
current = 0
65
else
66
current = tonumber(current)
67
end
68
69
local new_value = current + increment
70
redis.call('SET', key, new_value)
71
return new_value
72
"""
73
74
# Execute script directly
75
result1 = client.eval(increment_script, 1, 'counter', '5')
76
print(f"First increment: {result1}") # 5
77
78
result2 = client.eval(increment_script, 1, 'counter', '3')
79
print(f"Second increment: {result2}") # 8
80
81
# Verify the value
82
current_value = client.get('counter')
83
print(f"Current counter value: {current_value.decode()}") # 8
84
```
85
86
### Script Caching with SHA
87
88
```python
89
import fakeredis
90
91
client = fakeredis.FakeRedis()
92
93
# Load script and get SHA hash
94
multi_key_script = """
95
-- Get multiple keys and return them as a table
96
local keys = KEYS
97
local result = {}
98
99
for i, key in ipairs(keys) do
100
local value = redis.call('GET', key)
101
if value ~= false then
102
result[key] = value
103
end
104
end
105
106
return result
107
"""
108
109
# Load script into Redis cache
110
script_sha = client.script_load(multi_key_script)
111
print(f"Script loaded with SHA: {script_sha}")
112
113
# Set some test data
114
client.mset({
115
'user:1:name': 'Alice',
116
'user:1:email': 'alice@example.com',
117
'user:1:age': '30',
118
'nonexistent:key': 'wont_be_set'
119
})
120
121
# Execute using SHA (more efficient for repeated executions)
122
result = client.evalsha(
123
script_sha,
124
3, # number of keys
125
'user:1:name', 'user:1:email', 'user:1:age' # keys
126
)
127
128
print("Script result:")
129
for key, value in result.items():
130
print(f" {key}: {value}")
131
132
# Check if script exists in cache
133
exists = client.script_exists(script_sha)
134
print(f"Script exists in cache: {exists}") # [True]
135
```
136
137
### Atomic Operations with Lua
138
139
```python
140
import fakeredis
141
142
client = fakeredis.FakeRedis()
143
144
# Atomic transfer between accounts
145
transfer_script = """
146
local from_account = KEYS[1]
147
local to_account = KEYS[2]
148
local amount = tonumber(ARGV[1])
149
150
-- Get current balances
151
local from_balance = tonumber(redis.call('GET', from_account) or 0)
152
local to_balance = tonumber(redis.call('GET', to_account) or 0)
153
154
-- Check if sufficient funds
155
if from_balance < amount then
156
return {err = 'Insufficient funds'}
157
end
158
159
-- Perform transfer
160
redis.call('SET', from_account, from_balance - amount)
161
redis.call('SET', to_account, to_balance + amount)
162
163
-- Return new balances
164
return {
165
from_balance = from_balance - amount,
166
to_balance = to_balance + amount,
167
transferred = amount
168
}
169
"""
170
171
# Setup initial balances
172
client.set('account:alice', '1000')
173
client.set('account:bob', '500')
174
175
print("Initial balances:")
176
print(f"Alice: ${client.get('account:alice').decode()}")
177
print(f"Bob: ${client.get('account:bob').decode()}")
178
179
# Execute transfer
180
result = client.eval(
181
transfer_script,
182
2, # 2 keys
183
'account:alice', 'account:bob', # from, to
184
'150' # amount
185
)
186
187
print(f"\nTransfer result: {result}")
188
189
print("\nFinal balances:")
190
print(f"Alice: ${client.get('account:alice').decode()}")
191
print(f"Bob: ${client.get('account:bob').decode()}")
192
193
# Try transfer with insufficient funds
194
print("\n--- Testing insufficient funds ---")
195
result2 = client.eval(
196
transfer_script,
197
2,
198
'account:alice', 'account:bob',
199
'2000' # More than Alice has
200
)
201
202
print(f"Insufficient funds result: {result2}")
203
```
204
205
### Complex Data Structure Operations
206
207
```python
208
import fakeredis
209
import json
210
211
client = fakeredis.FakeRedis()
212
213
# Script for managing a shopping cart with inventory checking
214
shopping_cart_script = """
215
local user_id = ARGV[1]
216
local action = ARGV[2]
217
local product_id = ARGV[3]
218
local quantity = tonumber(ARGV[4]) or 0
219
220
local cart_key = 'cart:' .. user_id
221
local product_key = 'product:' .. product_id
222
local inventory_key = 'inventory:' .. product_id
223
224
if action == 'add' then
225
-- Check inventory
226
local available = tonumber(redis.call('GET', inventory_key) or 0)
227
local current_in_cart = tonumber(redis.call('HGET', cart_key, product_id) or 0)
228
229
if available < (current_in_cart + quantity) then
230
return {error = 'Insufficient inventory', available = available, requested = current_in_cart + quantity}
231
end
232
233
-- Add to cart
234
redis.call('HINCRBY', cart_key, product_id, quantity)
235
local new_quantity = tonumber(redis.call('HGET', cart_key, product_id))
236
237
return {success = true, product = product_id, quantity = new_quantity}
238
239
elseif action == 'remove' then
240
-- Remove from cart
241
local current = tonumber(redis.call('HGET', cart_key, product_id) or 0)
242
local to_remove = math.min(quantity, current)
243
244
if to_remove > 0 then
245
redis.call('HINCRBY', cart_key, product_id, -to_remove)
246
local remaining = tonumber(redis.call('HGET', cart_key, product_id))
247
248
-- Remove if quantity is 0
249
if remaining <= 0 then
250
redis.call('HDEL', cart_key, product_id)
251
remaining = 0
252
end
253
254
return {success = true, product = product_id, quantity = remaining, removed = to_remove}
255
else
256
return {error = 'Product not in cart'}
257
end
258
259
elseif action == 'get' then
260
-- Get cart contents
261
local cart = redis.call('HGETALL', cart_key)
262
local result = {}
263
264
for i = 1, #cart, 2 do
265
local prod_id = cart[i]
266
local qty = tonumber(cart[i + 1])
267
result[prod_id] = qty
268
end
269
270
return {cart = result}
271
272
elseif action == 'checkout' then
273
-- Atomic checkout process
274
local cart = redis.call('HGETALL', cart_key)
275
local total_cost = 0
276
local items = {}
277
278
-- Check all items and calculate cost
279
for i = 1, #cart, 2 do
280
local prod_id = cart[i]
281
local qty = tonumber(cart[i + 1])
282
283
-- Check inventory
284
local available = tonumber(redis.call('GET', inventory_key:gsub(product_id, prod_id)) or 0)
285
if available < qty then
286
return {error = 'Insufficient inventory for ' .. prod_id, available = available, needed = qty}
287
end
288
289
-- Get product price
290
local price = tonumber(redis.call('HGET', 'product:' .. prod_id, 'price') or 0)
291
local item_cost = price * qty
292
total_cost = total_cost + item_cost
293
294
table.insert(items, {product = prod_id, quantity = qty, price = price, total = item_cost})
295
end
296
297
-- Deduct from inventory and clear cart
298
for _, item in ipairs(items) do
299
redis.call('DECRBY', 'inventory:' .. item.product, item.quantity)
300
end
301
302
redis.call('DEL', cart_key)
303
304
-- Create order
305
local order_id = 'order:' .. redis.call('INCR', 'order_counter')
306
redis.call('HSET', order_id, 'user_id', user_id, 'total_cost', total_cost, 'timestamp', redis.call('TIME')[1])
307
308
return {success = true, order_id = order_id, total_cost = total_cost, items = items}
309
end
310
311
return {error = 'Invalid action'}
312
"""
313
314
# Setup products and inventory
315
products = {
316
'laptop': {'price': 999.99, 'inventory': 5},
317
'mouse': {'price': 29.99, 'inventory': 50},
318
'keyboard': {'price': 89.99, 'inventory': 20}
319
}
320
321
for product_id, data in products.items():
322
client.hset(f'product:{product_id}', 'price', str(data['price']))
323
client.set(f'inventory:{product_id}', str(data['inventory']))
324
325
# Test shopping cart operations
326
user_id = 'user123'
327
328
# Add items to cart
329
print("=== Adding Items to Cart ===")
330
result1 = client.eval(shopping_cart_script, 0, user_id, 'add', 'laptop', '2')
331
print(f"Add laptop: {result1}")
332
333
result2 = client.eval(shopping_cart_script, 0, user_id, 'add', 'mouse', '3')
334
print(f"Add mouse: {result2}")
335
336
# Try to add more than available inventory
337
result3 = client.eval(shopping_cart_script, 0, user_id, 'add', 'laptop', '10')
338
print(f"Add too many laptops: {result3}")
339
340
# Get cart contents
341
cart_contents = client.eval(shopping_cart_script, 0, user_id, 'get')
342
print(f"Cart contents: {cart_contents}")
343
344
# Remove some items
345
result4 = client.eval(shopping_cart_script, 0, user_id, 'remove', 'laptop', '1')
346
print(f"Remove laptop: {result4}")
347
348
# Checkout
349
print("\n=== Checkout Process ===")
350
checkout_result = client.eval(shopping_cart_script, 0, user_id, 'checkout')
351
print(f"Checkout result: {checkout_result}")
352
353
# Check remaining inventory
354
print(f"\nRemaining inventory:")
355
for product_id in products:
356
remaining = client.get(f'inventory:{product_id}').decode()
357
print(f" {product_id}: {remaining}")
358
```
359
360
### Script Error Handling
361
362
```python
363
import fakeredis
364
365
client = fakeredis.FakeRedis()
366
367
# Script with error handling
368
safe_script = """
369
-- Safe division with error handling
370
local dividend = tonumber(ARGV[1])
371
local divisor = tonumber(ARGV[2])
372
373
-- Input validation
374
if not dividend or not divisor then
375
return {error = 'Invalid input: arguments must be numbers'}
376
end
377
378
if divisor == 0 then
379
return {error = 'Division by zero is not allowed'}
380
end
381
382
-- Perform calculation
383
local result = dividend / divisor
384
385
-- Store result with timestamp
386
local result_key = KEYS[1]
387
redis.call('HSET', result_key, 'result', result, 'timestamp', redis.call('TIME')[1])
388
389
return {success = true, result = result}
390
"""
391
392
# Test valid operations
393
print("=== Testing Valid Operations ===")
394
result1 = client.eval(safe_script, 1, 'calc:result1', '10', '2')
395
print(f"10 / 2 = {result1}")
396
397
result2 = client.eval(safe_script, 1, 'calc:result2', '7.5', '1.5')
398
print(f"7.5 / 1.5 = {result2}")
399
400
# Test error conditions
401
print("\n=== Testing Error Conditions ===")
402
result3 = client.eval(safe_script, 1, 'calc:result3', '10', '0')
403
print(f"10 / 0 = {result3}")
404
405
result4 = client.eval(safe_script, 1, 'calc:result4', 'abc', '5')
406
print(f"'abc' / 5 = {result4}")
407
408
# Check stored results
409
print("\n=== Stored Results ===")
410
for i in [1, 2]:
411
stored = client.hgetall(f'calc:result{i}')
412
if stored:
413
result_val = stored[b'result'].decode()
414
timestamp = stored[b'timestamp'].decode()
415
print(f"Result {i}: {result_val} (calculated at {timestamp})")
416
```
417
418
### Advanced Lua Features
419
420
```python
421
import fakeredis
422
import time
423
424
client = fakeredis.FakeRedis()
425
426
# Advanced script using multiple Redis data types
427
analytics_script = """
428
-- Advanced analytics script
429
local event_type = ARGV[1]
430
local user_id = ARGV[2]
431
local timestamp = tonumber(ARGV[3])
432
local metadata = cjson.decode(ARGV[4])
433
434
-- Keys for different data structures
435
local daily_events_key = 'analytics:daily:' .. os.date('%Y-%m-%d', timestamp)
436
local user_events_key = 'analytics:user:' .. user_id
437
local event_stream_key = 'analytics:stream:' .. event_type
438
local leaderboard_key = 'analytics:leaderboard:' .. event_type
439
440
-- 1. Increment daily event counter (Hash)
441
redis.call('HINCRBY', daily_events_key, event_type, 1)
442
443
-- 2. Add to user's event list (List with capping)
444
local event_data = cjson.encode({
445
type = event_type,
446
timestamp = timestamp,
447
metadata = metadata
448
})
449
redis.call('LPUSH', user_events_key, event_data)
450
redis.call('LTRIM', user_events_key, 0, 99) -- Keep only last 100 events
451
452
-- 3. Add to event stream (Stream)
453
local stream_id = redis.call('XADD', event_stream_key, '*',
454
'user_id', user_id,
455
'timestamp', timestamp,
456
'metadata', ARGV[4]
457
)
458
459
-- 4. Update user score in leaderboard (Sorted Set)
460
local score_increment = metadata.score or 1
461
redis.call('ZINCRBY', leaderboard_key, score_increment, user_id)
462
463
-- 5. Set key expiration for cleanup
464
redis.call('EXPIRE', daily_events_key, 86400 * 30) -- 30 days
465
redis.call('EXPIRE', user_events_key, 86400 * 7) -- 7 days
466
467
-- 6. Get current statistics
468
local daily_stats = redis.call('HGETALL', daily_events_key)
469
local user_score = redis.call('ZSCORE', leaderboard_key, user_id)
470
local top_users = redis.call('ZREVRANGE', leaderboard_key, 0, 4, 'WITHSCORES')
471
472
-- Format response
473
local stats = {}
474
for i = 1, #daily_stats, 2 do
475
stats[daily_stats[i]] = tonumber(daily_stats[i + 1])
476
end
477
478
local leaderboard = {}
479
for i = 1, #top_users, 2 do
480
table.insert(leaderboard, {
481
user = top_users[i],
482
score = tonumber(top_users[i + 1])
483
})
484
end
485
486
return {
487
stream_id = stream_id,
488
user_score = tonumber(user_score),
489
daily_stats = stats,
490
top_users = leaderboard
491
}
492
"""
493
494
# Test the analytics script
495
print("=== Advanced Analytics Test ===")
496
497
# Simulate multiple events
498
events = [
499
('page_view', 'user1', {'score': 1, 'page': '/home'}),
500
('purchase', 'user1', {'score': 10, 'amount': 99.99}),
501
('page_view', 'user2', {'score': 1, 'page': '/products'}),
502
('signup', 'user3', {'score': 5, 'referrer': 'google'}),
503
('purchase', 'user2', {'score': 15, 'amount': 149.99}),
504
('page_view', 'user1', {'score': 1, 'page': '/checkout'})
505
]
506
507
for event_type, user_id, metadata in events:
508
result = client.eval(
509
analytics_script,
510
0, # No KEYS needed
511
event_type,
512
user_id,
513
str(int(time.time())),
514
json.dumps(metadata)
515
)
516
517
print(f"\n{event_type} by {user_id}:")
518
print(f" Stream ID: {result['stream_id']}")
519
print(f" User Score: {result['user_score']}")
520
print(f" Daily Stats: {result['daily_stats']}")
521
print(f" Top Users: {result['top_users']}")
522
```
523
524
### Pattern: Rate Limiting with Lua
525
526
```python
527
import fakeredis
528
import time
529
import threading
530
531
class LuaRateLimiter:
532
def __init__(self, client: fakeredis.FakeRedis):
533
self.client = client
534
535
# Sliding window rate limiter script
536
self.rate_limit_script = """
537
local key = KEYS[1]
538
local window = tonumber(ARGV[1])
539
local limit = tonumber(ARGV[2])
540
local current_time = tonumber(ARGV[3])
541
542
-- Remove expired entries
543
redis.call('ZREMRANGEBYSCORE', key, 0, current_time - window)
544
545
-- Count current requests
546
local current_requests = redis.call('ZCARD', key)
547
548
if current_requests < limit then
549
-- Add current request
550
redis.call('ZADD', key, current_time, current_time)
551
redis.call('EXPIRE', key, math.ceil(window))
552
return {allowed = true, remaining = limit - current_requests - 1}
553
else
554
return {allowed = false, remaining = 0, retry_after = redis.call('ZRANGE', key, 0, 0)[1] + window - current_time}
555
end
556
"""
557
558
self.script_sha = self.client.script_load(self.rate_limit_script)
559
560
def is_allowed(self, identifier: str, window_seconds: int, max_requests: int) -> dict:
561
"""Check if request is allowed under rate limit"""
562
key = f"rate_limit:{identifier}"
563
current_time = time.time()
564
565
result = self.client.evalsha(
566
self.script_sha,
567
1,
568
key,
569
str(window_seconds),
570
str(max_requests),
571
str(current_time)
572
)
573
574
return result
575
576
# Usage example
577
client = fakeredis.FakeRedis()
578
rate_limiter = LuaRateLimiter(client)
579
580
def simulate_api_requests(user_id: str, num_requests: int):
581
"""Simulate API requests from a user"""
582
print(f"\n=== User {user_id} making {num_requests} requests ===")
583
584
for i in range(num_requests):
585
# Rate limit: 5 requests per 10 seconds
586
result = rate_limiter.is_allowed(user_id, window_seconds=10, max_requests=5)
587
588
if result['allowed']:
589
print(f"Request {i+1}: ✅ Allowed (remaining: {result['remaining']})")
590
else:
591
retry_after = result.get('retry_after', 0)
592
print(f"Request {i+1}: ❌ Rate limited (retry after: {retry_after:.1f}s)")
593
594
time.sleep(0.5) # Small delay between requests
595
596
# Test rate limiting
597
simulate_api_requests('user1', 8) # Should hit rate limit
598
599
print("\n=== Testing Multiple Users ===")
600
601
def concurrent_requests(user_id: str):
602
"""Concurrent requests from different users"""
603
for i in range(3):
604
result = rate_limiter.is_allowed(user_id, window_seconds=10, max_requests=5)
605
status = "✅" if result['allowed'] else "❌"
606
print(f"User {user_id} Request {i+1}: {status}")
607
time.sleep(0.2)
608
609
# Test concurrent access
610
users = ['userA', 'userB', 'userC']
611
threads = []
612
613
for user_id in users:
614
thread = threading.Thread(target=concurrent_requests, args=(user_id,))
615
threads.append(thread)
616
thread.start()
617
618
for thread in threads:
619
thread.join()
620
```
621
622
### Pattern: Custom Data Structures
623
624
```python
625
import fakeredis
626
627
client = fakeredis.FakeRedis()
628
629
# Implement a Lua-based priority queue
630
priority_queue_script = """
631
local action = ARGV[1]
632
local queue_key = KEYS[1]
633
634
if action == 'enqueue' then
635
local item = ARGV[2]
636
local priority = tonumber(ARGV[3])
637
638
-- Add item with priority as score (higher score = higher priority)
639
redis.call('ZADD', queue_key, priority, item)
640
641
return redis.call('ZCARD', queue_key)
642
643
elseif action == 'dequeue' then
644
-- Get highest priority item
645
local items = redis.call('ZREVRANGE', queue_key, 0, 0, 'WITHSCORES')
646
647
if #items == 0 then
648
return nil
649
end
650
651
local item = items[1]
652
local priority = tonumber(items[2])
653
654
-- Remove the item
655
redis.call('ZREM', queue_key, item)
656
657
return {item = item, priority = priority}
658
659
elseif action == 'peek' then
660
-- Get highest priority item without removing
661
local items = redis.call('ZREVRANGE', queue_key, 0, 0, 'WITHSCORES')
662
663
if #items == 0 then
664
return nil
665
end
666
667
return {item = items[1], priority = tonumber(items[2])}
668
669
elseif action == 'size' then
670
return redis.call('ZCARD', queue_key)
671
672
elseif action == 'list' then
673
-- Get all items sorted by priority
674
local items = redis.call('ZREVRANGE', queue_key, 0, -1, 'WITHSCORES')
675
local result = {}
676
677
for i = 1, #items, 2 do
678
table.insert(result, {
679
item = items[i],
680
priority = tonumber(items[i + 1])
681
})
682
end
683
684
return result
685
end
686
687
return {error = 'Invalid action'}
688
"""
689
690
class LuaPriorityQueue:
691
def __init__(self, client: fakeredis.FakeRedis, queue_name: str):
692
self.client = client
693
self.queue_key = f"priority_queue:{queue_name}"
694
self.script_sha = self.client.script_load(priority_queue_script)
695
696
def enqueue(self, item: str, priority: int) -> int:
697
"""Add item with priority to queue"""
698
return self.client.evalsha(self.script_sha, 1, self.queue_key, 'enqueue', item, str(priority))
699
700
def dequeue(self):
701
"""Remove and return highest priority item"""
702
return self.client.evalsha(self.script_sha, 1, self.queue_key, 'dequeue')
703
704
def peek(self):
705
"""Get highest priority item without removing"""
706
return self.client.evalsha(self.script_sha, 1, self.queue_key, 'peek')
707
708
def size(self) -> int:
709
"""Get queue size"""
710
return self.client.evalsha(self.script_sha, 1, self.queue_key, 'size')
711
712
def list_all(self):
713
"""Get all items sorted by priority"""
714
return self.client.evalsha(self.script_sha, 1, self.queue_key, 'list')
715
716
# Test the priority queue
717
print("=== Priority Queue Test ===")
718
719
pq = LuaPriorityQueue(client, 'tasks')
720
721
# Add tasks with different priorities
722
tasks = [
723
('Send email', 3),
724
('Critical bug fix', 10),
725
('Update documentation', 2),
726
('Deploy to production', 9),
727
('Code review', 5),
728
('Security patch', 10)
729
]
730
731
print("Enqueuing tasks:")
732
for task, priority in tasks:
733
size = pq.enqueue(task, priority)
734
print(f" Added '{task}' (priority {priority}) - Queue size: {size}")
735
736
print(f"\nQueue contents (sorted by priority):")
737
all_tasks = pq.list_all()
738
for task_info in all_tasks:
739
print(f" Priority {task_info['priority']}: {task_info['item']}")
740
741
print(f"\nProcessing tasks:")
742
while pq.size() > 0:
743
# Peek at next task
744
next_task = pq.peek()
745
print(f" Next: {next_task['item']} (priority {next_task['priority']})")
746
747
# Dequeue and process
748
processed = pq.dequeue()
749
print(f" Processed: {processed['item']}")
750
751
if pq.size() > 0:
752
print(f" Remaining tasks: {pq.size()}")
753
754
print("All tasks completed!")
755
```