or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

bitmap-operations.mdcore-clients.mdgeneric-operations.mdgeospatial-operations.mdhash-operations.mdindex.mdlist-operations.mdlua-scripting.mdpubsub-operations.mdserver-management.mdserver-operations.mdset-operations.mdsorted-set-operations.mdstack-extensions.mdstream-operations.mdstring-operations.mdtransaction-operations.mdvalkey-support.md

lua-scripting.mddocs/

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

```