Job Middleware and Hook Patterns
In this tutorial, you will learn about Job Middleware and Hook Patterns. We cover key concepts, practical examples, and best practices to help you master this topic.
Implement middleware hooks for background job processing including before/after hooks, around filters, error hooks, and middleware pipelines for cross-cutting concerns.
What You Learn
You will learn how to build a middleware pipeline for job processing, implement before/after hooks for logging and metrics, create error hooks for failure handling, and compose middleware modules.
Why It Matters
Cross-cutting concerns like logging, metrics, authentication, and Rate Limiting should not be duplicated in every job. Middleware hooks apply them consistently across all jobs without code duplication.
Real-World Use
DodaTech's job system uses middleware for: logging every job execution, collecting duration metrics, tracking rate limits per user, and automatically retrying transient failures. Each middleware is a reusable module.
Middleware Pipeline Architecture
flowchart TD
J[Job Arrives] --> M1[Metrics Middleware]
M1 --> M2[Logging Middleware]
M2 --> M3[Auth Middleware]
M3 --> M4[Rate Limit Middleware]
M4 --> H[Job Handler]
H --> M5[Cleanup Middleware]
M5 --> E[Complete]
H -.->|Error| EH[Error Middleware]
EH --> E
Middleware Pipeline
import time
import functools
class MiddlewarePipeline:
def __init__(self):
self._middlewares = []
self._handlers = {}
def use(self, middleware):
self._middlewares.append(middleware)
return self
def handler(self, name):
def decorator(func):
self._handlers[name] = func
return func
return decorator
def execute(self, name, **context):
handler = self._handlers.get(name)
if not handler:
raise ValueError(f"Unknown handler: {name}")
def create_chain(middlewares, final_handler):
if not middlewares:
return final_handler
mw = middlewares[0]
rest = middlewares[1:]
return lambda ctx: mw(ctx, create_chain(rest, final_handler))
chain = create_chain(self._middlewares, handler)
return chain(context)
class MetricsMiddleware:
def __call__(self, context, next_middleware):
start = time.time()
context['metrics'] = {'started_at': start}
try:
result = next_middleware(context)
duration = time.time() - start
context['metrics']['duration'] = duration
print(f" Metrics: {context.get('job_type', 'job')} took {duration:.2f}s")
return result
except Exception as e:
duration = time.time() - start
context['metrics']['duration'] = duration
context['metrics']['error'] = str(e)
print(f" Metrics: {context.get('job_type', 'job')} FAILED after {duration:.2f}s")
raise
class LoggingMiddleware:
def __call__(self, context, next_middleware):
print(f" Log: Starting job {context.get('job_id', 'unknown')}")
try:
result = next_middleware(context)
print(f" Log: Completed job {context.get('job_id', 'unknown')}")
return result
except Exception as e:
print(f" Log: Failed job {context.get('job_id', 'unknown')}: {e}")
raise
pipeline = MiddlewarePipeline()
@pipeline.handler('scan')
def scan_job(context):
print(f" Scanning: {context['file']}")
time.sleep(0.2)
return {'threats': 0}
pipeline.use(MetricsMiddleware())
pipeline.use(LoggingMiddleware())
result = pipeline.execute('scan', job_id='scan-001', job_type='file_scan',
file='document.pdf')
print(f"Result: {result}")
Expected output:
Log: Starting job scan-001
Scanning: document.pdf
Log: Completed job scan-001
Metrics: file_scan took 0.20s
Result: {'threats': 0}
Before/After Hooks
import time
class HookedJob:
def __init__(self):
self.before_hooks = []
self.after_hooks = []
self.error_hooks = []
def before(self, func):
self.before_hooks.append(func)
return func
def after(self, func):
self.after_hooks.append(func)
return func
def on_error(self, func):
self.error_hooks.append(func)
return func
def execute(self, job_name, job_func, **kwargs):
context = {'job_name': job_name, 'started_at': time.time(), **kwargs}
for hook in self.before_hooks:
hook(context)
try:
result = job_func(context)
context['result'] = result
for hook in self.after_hooks:
hook(context)
return result
except Exception as e:
context['error'] = str(e)
for hook in self.error_hooks:
hook(context)
raise
hooked = HookedJob()
@hooked.before
def log_start(ctx):
print(f"[{ctx['job_name']}] Starting at {time.strftime('%H:%M:%S')}")
@hooked.before
def increment_counter(ctx):
print(f"[{ctx['job_name']}] Counter incremented")
@hooked.after
def log_duration(ctx):
duration = time.time() - ctx['started_at']
print(f"[{ctx['job_name']}] Completed in {duration:.2f}s")
@hooked.on_error
def log_error(ctx):
print(f"[{ctx['job_name']}] FAILED: {ctx['error']}")
def process_job(ctx):
print(f" Processing {ctx.get('file', 'unknown')}")
time.sleep(0.1)
return {'status': 'ok'}
hooked.execute('file_scan', process_job, file='report.pdf')
Expected output:
[file_scan] Starting at 10:00:00
[file_scan] Counter incremented
Processing report.pdf
[file_scan] Completed in 0.10s
Around Filters
import time
class AroundFilter:
def __init__(self):
self.filters = []
def add(self, name, filter_func):
self.filters.append((name, filter_func))
return self
def execute(self, job_func, **kwargs):
chain = job_func
for name, filter_func in reversed(self.filters):
def make_filter(fn, fname, ff):
def filtered(**kw):
print(f" [{fname}] before")
start = time.time()
try:
result = fn(**kw)
duration = time.time() - start
print(f" [{fname}] after ({duration:.2f}s)")
return result
except Exception as e:
print(f" [{fname}] error: {e}")
raise
return filtered
chain = make_filter(chain, name, filter_func)
return chain(**kwargs)
around = AroundFilter()
def db_transaction(next_func, **kwargs):
print(" [DB] Transaction started")
try:
result = next_func(**kwargs)
print(" [DB] Transaction committed")
return result
except Exception as e:
print(" [DB] Transaction rolled back")
raise
def timing(next_func, **kwargs):
start = time.time()
result = next_func(**kwargs)
print(f" [Timing] Duration: {time.time() - start:.2f}s")
return result
around.add('db', db_transaction).add('timing', timing)
def process_payment(amount, account):
print(f" Processing payment ${amount} to {account}")
return {'txn_id': 'txn_001'}
result = around.execute(process_payment, amount=100, account='acc-123')
print(f"Result: {result}")
Expected output:
[db] before
[timing] before
Processing payment $100 to acc-123
[timing] after (0.00s)
[db] after (0.00s)
Result: {'txn_id': 'txn_001'}
Error Handling Middleware
import time
class ErrorHandlingMiddleware:
def __init__(self):
self.error_handlers = {}
def catch(self, exception_type):
def decorator(func):
self.error_handlers[exception_type] = func
return func
return decorator
def execute(self, job_func, **kwargs):
try:
return job_func(**kwargs)
except tuple(self.error_handlers.keys()) as e:
handler = self.error_handlers[type(e)]
result = handler(e, kwargs)
print(f" Handled {type(e).__name__}: {e}")
return result
class NetworkError(Exception):
pass
class TimeoutError(Exception):
pass
def process_data(data):
if data.get('type') == 'timeout':
raise TimeoutError("Connection timed out")
if data.get('type') == 'network':
raise NetworkError("Network unreachable")
return {'processed': data}
ehm = ErrorHandlingMiddleware()
@ehm.catch(NetworkError)
def handle_network(err, context):
print(f" Retrying network operation...")
return {'status': 'retried'}
@ehm.catch(TimeoutError)
def handle_timeout(err, context):
print(f" Scheduling retry for later...")
return {'status': 'scheduled_retry'}
result1 = ehm.execute(process_data, data={'type': 'network'})
result2 = ehm.execute(process_data, data={'type': 'timeout'})
print(f"Results: {result1}, {result2}")
Expected output:
Retrying network operation...
Handled NetworkError: Network unreachable
Scheduling retry for later...
Handled TimeoutError: Connection timed out
Results: {'status': 'retried'}, {'status': 'scheduled_retry'}
Common Mistakes
1. Middleware State Leakage
Middleware that modifies shared state across jobs causes race conditions. Each middleware invocation should use clean context.
2. Order-Dependent Middleware
Relying on middleware execution order creates hidden coupling. Document the expected order and validate it at startup.
3. Error Swallowing
Middleware that catches all exceptions without re-raising can hide critical failures. Re-raise after handling or logging.
4. Heavy Middleware
Slow middleware (external API calls, heavy computations) blocks the entire pipeline. Keep middleware fast or offload heavy work.
5. No Middleware Testing
Middleware affects all jobs. A bug in middleware breaks every job. Test middleware in isolation and integration.
Practice Questions
1. What is the middleware pattern in job processing?
A pipeline of functions that wrap job execution. Each middleware handles a cross-cutting concern and calls the next middleware or final handler.
2. How do before/after hooks differ from around filters?
Before/after hooks run at specific points. Around filters wrap the entire execution and can modify input and output.
3. Why is middleware order important?
Some middleware depends on context set by earlier middleware. Metrics middleware must wrap everything to measure total duration.
4. How do error hooks work in middleware?
When the handler raises an exception, error hooks run instead of after hooks. They can log, cleanup, or provide fallback results.
Challenge
Build a middleware system for a payment processing queue with: logging middleware (log every job), metrics middleware (duration, success/failure), retry middleware (auto-retry transient failures), rate limit middleware (per-user limits), and Transaction middleware (DB commit/rollback).
FAQ
Mini Project: Middleware Pipeline
import time
class MiddlewarePipeline:
def __init__(self):
self.layers = []
def add(self, name, before=None, after=None, error=None):
self.layers.append({
'name': name, 'before': before, 'after': after, 'error': error
})
def run(self, handler, context):
context['_start'] = time.time()
for layer in self.layers:
if layer['before']:
layer['before'](context)
try:
result = handler(context)
context['result'] = result
except Exception as e:
context['error'] = str(e)
for layer in reversed(self.layers):
if layer['error']:
layer['error'](context)
raise
for layer in reversed(self.layers):
if layer['after']:
layer['after'](context)
return context.get('result')
pipe = MiddlewarePipeline()
pipe.add('logging',
before=lambda c: print(f"Start {c['name']}"),
after=lambda c: print(f"End {c['name']} ({time.time()-c['_start']:.2f}s)"),
error=lambda c: print(f"Error {c['name']}: {c['error']}"))
pipe.add('metrics',
after=lambda c: print(f" Duration: {time.time()-c['_start']:.2f}s"))
pipe.run(lambda c: time.sleep(0.1) or 'done', {'name': 'test_job'})
Expected output:
Start test_job
End test_job (0.10s)
Duration: 0.10s
What's Next
Now that you understand middleware, explore job lifecycle events for event-driven processing, then learn about job metrics with Prometheus for monitoring.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro