SimPy Interrupts Explained: Handling the Unexpected

Real systems have interruptions. Machines break. Priorities change. Shifts end. SimPy lets you model all of this with interrupts.

What Is an Interrupt?

An interrupt forces a waiting process to stop waiting and handle the interruption:

import simpy

def worker(env):
    try:
        print(f"Working at {env.now}")
        yield env.timeout(100)  # Long task
        print(f"Finished at {env.now}")
    except simpy.Interrupt as interrupt:
        print(f"Interrupted at {env.now}: {interrupt.cause}")

def interrupter(env, worker_process):
    yield env.timeout(10)
    worker_process.interrupt("Break time!")

env = simpy.Environment()
worker_proc = env.process(worker(env))
env.process(interrupter(env, worker_proc))
env.run()

Output:

Working at 0
Interrupted at 10: Break time!

When to Use Interrupts

Interrupts model: - Machine breakdowns - Failure during operation - Preemption - Higher priority takes over - Cancellation - Customer leaves, order cancelled - Timeouts - Giving up after waiting too long - Shift changes - Worker goes home

Basic Interrupt Pattern

def interruptible_process(env):
    while True:
        try:
            print(f"Starting task at {env.now}")
            yield env.timeout(10)
            print(f"Completed task at {env.now}")
        except simpy.Interrupt:
            print(f"Task interrupted at {env.now}")

Always wrap potentially-interrupted waits in try/except.

Interrupt with Cause

Pass information about why the interrupt happened:

def maintenance(env, machine_process):
    yield env.timeout(50)
    machine_process.interrupt(cause={
        'type': 'maintenance',
        'duration': 20
    })

def machine(env):
    try:
        yield env.timeout(100)
    except simpy.Interrupt as i:
        if i.cause['type'] == 'maintenance':
            print(f"Maintenance for {i.cause['duration']} units")
            yield env.timeout(i.cause['duration'])

Resuming After Interrupt

Continue where you left off:

def resilient_worker(env, total_work):
    remaining = total_work

    while remaining > 0:
        try:
            start = env.now
            yield env.timeout(remaining)
            remaining = 0  # Completed
            print(f"Work complete at {env.now}")
        except simpy.Interrupt:
            elapsed = env.now - start
            remaining -= elapsed
            print(f"Interrupted. {remaining} remaining")

Machine Breakdown Example

import random

def machine(env, name, repair_time_mean):
    """Machine that occasionally breaks down."""
    while True:
        # Working
        try:
            processing_time = random.expovariate(1/10)
            print(f"{name} processing for {processing_time:.1f}")
            yield env.timeout(processing_time)
            print(f"{name} completed part at {env.now:.1f}")
        except simpy.Interrupt:
            print(f"{name} broke down at {env.now:.1f}")
            repair_time = random.expovariate(1/repair_time_mean)
            yield env.timeout(repair_time)
            print(f"{name} repaired at {env.now:.1f}")

def breakdown_generator(env, machine_process, mean_time_to_fail):
    """Generates breakdowns at random intervals."""
    while True:
        yield env.timeout(random.expovariate(1/mean_time_to_fail))
        if not machine_process.is_alive:
            continue  # Machine process ended
        machine_process.interrupt("Breakdown!")

env = simpy.Environment()
machine_proc = env.process(machine(env, "Lathe", repair_time_mean=5))
env.process(breakdown_generator(env, machine_proc, mean_time_to_fail=30))
env.run(until=100)

Customer Reneging with Interrupt

Customer gives up waiting:

def customer(env, name, server, patience):
    """Customer who will leave if wait is too long."""
    with server.request() as req:
        # Wait for service or give up
        result = yield req | env.timeout(patience)

        if req in result:
            # Got service
            yield env.timeout(random.expovariate(1/5))
            print(f"{name} served at {env.now:.1f}")
        else:
            # Patience ran out
            print(f"{name} left at {env.now:.1f} (waited too long)")

This uses event composition, not explicit interrupts—but achieves the same goal.

Interrupt vs Event Composition

Two ways to handle conditional waits:

Interrupt approach:

def waiter(env):
    try:
        yield env.timeout(100)
    except simpy.Interrupt:
        print("Interrupted!")

def interrupter(env, proc):
    yield env.timeout(10)
    proc.interrupt()

Event composition approach:

def process(env):
    condition = env.timeout(10)
    long_wait = env.timeout(100)

    result = yield condition | long_wait
    if condition in result:
        print("Condition met first")

Use interrupts when: - Another process decides to interrupt - You need to model external intervention

Use event composition when: - The process itself decides based on conditions - Simpler logic suffices

Checking If Process Is Alive

Before interrupting:

if process.is_alive:
    process.interrupt("cause")

Interrupting a dead process raises an error.

Nested Try/Except

Handle multiple interrupt sources:

def complex_worker(env):
    while True:
        try:
            yield env.timeout(10)
        except simpy.Interrupt as i:
            if i.cause == 'break':
                yield env.timeout(5)  # Take break
            elif i.cause == 'emergency':
                yield env.timeout(20)  # Handle emergency
            elif i.cause == 'shutdown':
                return  # Exit completely

Interrupt Timing

Interrupts occur immediately when called:

def printer(env):
    try:
        yield env.timeout(10)
    except simpy.Interrupt:
        print(f"Interrupted at exactly {env.now}")

def interrupter(env, proc):
    yield env.timeout(5.5)
    proc.interrupt()

# Interrupt happens at exactly t=5.5

Common Pitfalls

Not Handling Interrupts

# Bad - unhandled interrupt crashes simulation
def fragile(env):
    yield env.timeout(100)

# Good - always handle if interrupts possible
def robust(env):
    try:
        yield env.timeout(100)
    except simpy.Interrupt:
        pass  # Handle appropriately

Interrupting Dead Process

# Bad - raises RuntimeError
proc.interrupt()  # But proc already finished

# Good - check first
if proc.is_alive:
    proc.interrupt()

Infinite Interrupt Loop

# Bad - immediately re-interrupted forever
def bad_handler(env):
    while True:
        try:
            yield env.timeout(10)
        except simpy.Interrupt:
            pass  # Should yield something!

# Good - yield after handling
def good_handler(env):
    while True:
        try:
            yield env.timeout(10)
        except simpy.Interrupt:
            yield env.timeout(1)  # Recovery time

Summary

Interrupts: - Model external interventions - Use process.interrupt(cause) to trigger - Handle with try/except simpy.Interrupt - Check process.is_alive before interrupting - Can include information via cause

When the unexpected happens, interrupt.

Next Steps


Discover the Power of Simulation

Want to become a go-to expert in simulation with Python? The Complete Simulation Bootcamp will show you how simulation can transform your career and your projects.

Explore the Bootcamp