Nyimbi Odero

Series 01 · a numbered run · kept in public

Lesson 10: Fault Tolerance and Recovery in Distributed Systems

Welcome back! In this lesson, we’ll focus on building fault tolerance and recovery mechanisms into distributed systems. These mechanisms ensure that your workflows can recover from failures and maintain data integrity across services. In distributed systems like ERP/CRM platforms or multi-sided marketplaces, failures are inevitable---whether they are network outages, server crashes, or task failures---so it’s critical to design systems that can handle these failures gracefully.

Table of Contents

  1. Introduction to Fault Tolerance and Recovery

  2. Strategies for Fault Tolerance

  3. Implementing Fault Tolerance in the DSL

  4. Using Celery for Task Retry and Failure Handling

  5. Database Transactions and Rollback Mechanisms

  6. Practical Example: Fault-Tolerant Order Fulfillment System

  7. Conclusion and Next Steps

1. Introduction to Fault Tolerance and Recovery

What is Fault Tolerance?

Fault tolerance is the ability of a system to continue functioning in the event of a failure. In distributed systems, failures can occur at different levels:

  • Network Failures: When nodes can’t communicate due to network issues.

  • Service Failures: When a service crashes or becomes unresponsive.

  • Task Failures: When individual tasks (e.g., database queries, API calls) fail.

Fault-tolerant systems are designed to handle these failures without interrupting the overall workflow or system operation.

What is Recovery?

Recovery involves bringing the system back to a consistent state after a failure. Recovery mechanisms can include retrying failed tasks, rolling back partially completed transactions, and synchronizing the state of the system across services.

2. Strategies for Fault Tolerance

Key Strategies for Fault Tolerance in Distributed Systems

  1. Task Retries: Automatically retry failed tasks with a delay between retries, typically with exponential backoff.

  2. Idempotency: Ensure that retrying a task multiple times has the same effect as running it once (i.e., the task produces consistent results).

  3. Circuit Breakers: Temporarily stop executing certain tasks or operations when failures reach a threshold.

  4. Fallbacks: Provide alternative actions or services in case of failure.

  5. Database Transactions: Use transactions to ensure that all operations within a workflow either succeed or fail as a unit.

  6. Compensation Transactions: Roll back previously completed steps if a workflow cannot complete successfully.

3. Implementing Fault Tolerance in the DSL

Extending the DSL to Support Fault Tolerance

We can extend our DSL to define fault-tolerant workflows by specifying retry policies, timeouts, and fallback actions. This makes it easier for non-technical stakeholders to configure fault tolerance for workflows without diving into the underlying code.

DSL Syntax for Fault Tolerance:

workflow OrderProcessing {

state Pending {

transition approve -> Approved by Manager;

transition reject -> Rejected by Manager;

}

state Approved {

concurrent task notifyWarehouse, notifyCustomer -> Completed retry 3 times, timeout 5 seconds;

}

state Completed {

event shipmentConfirmed -> Shipped by LogisticsSystem;

}

state Shipped {

// End of workflow

}

}

Key Additions:

  • Retry Policy: Specifies how many times to retry a failed task (e.g., retry 3 times).

  • Timeout: Specifies how long the system should wait before marking the task as failed (e.g., timeout 5 seconds).

4. Using Celery for Task Retry and Failure Handling

Celery’s Built-in Retry Mechanisms

Celery provides a robust retry mechanism that allows tasks to be retried in the event of failure. You can configure tasks to automatically retry when an exception is raised, specify the number of retries, and control the delay between retries.

Step 1: Defining a Task with Retry Logic in Celery

Here’s how to implement retry logic in Celery:

Example: Task with Retry in Celery:

from celery import Celery

celery = Celery(‘tasks’, broker=‘redis://localhost:6379/0’)

@celery.task(bind=True, max_retries=3, default_retry_delay=5)

def notify_warehouse(self, order_id):

try:

Simulate a task that may fail

raise Exception(“Warehouse service is unavailable”)

except Exception as exc:

Retry the task if it fails

raise self.retry(exc=exc)

@celery.task(bind=True, max_retries=3, default_retry_delay=5)

def notify_customer(self, order_id):

try:

print(f”Notifying customer about order {order_id}”)

Simulate success

except Exception as exc:

raise self.retry(exc=exc)

Explanation:

  • max_retries=3: Specifies the maximum number of times the task will be retried.

  • default_retry_delay=5: Specifies a 5-second delay between retries.

  • self.retry: Celery’s retry mechanism, which raises the task for re-execution if it fails.

Step 2: Integrating Fault Tolerance into the Workflow

Here’s how you can integrate the fault-tolerant tasks into the generated workflow code:

Example: Fault-Tolerant Workflow Execution:

@app.route(‘/order/int:order_id/fulfill’, methods=

′POST′'POST'

)

def fulfill_order(order_id):

workflow = OrderProcessingWorkflow()

try:

workflow.fulfill(‘FulfillmentSystem’)

Run distributed tasks with retry logic

notify_warehouse.delay(order_id)

notify_customer.delay(order_id)

Update the order status in the database

order = Order.query.get(order_id)

order.state = ‘Completed’

db.session.commit()

return jsonify({‘status’: ‘success’, ‘new_state’: ‘Completed’}), 200

except ValueError as e:

return jsonify({‘status’: ‘error’, ‘message’: str(e)}), 400

Now, if notify_warehouse or notify_customer fails, Celery will retry the tasks up to 3 times with a 5-second delay between retries.

5. Database Transactions and Rollback Mechanisms

Using Database Transactions for Atomicity

Database transactions are essential for ensuring atomicity in workflows. Atomicity guarantees that all steps in a workflow either complete successfully or fail as a unit. If any part of the workflow fails, the system should roll back all changes to avoid partial updates.

Step 1: Implementing Transactions in Flask with SQLAlchemy

SQLAlchemy provides transaction management, which allows you to roll back changes if an error occurs during workflow execution.

Example: Using Transactions in Flask:

@app.route(‘/order/int:order_id/fulfill’, methods=

′POST′'POST'

)

def fulfill_order(order_id):

workflow = OrderProcessingWorkflow()

try:

Start a database transaction

with db.session.begin(subtransactions=True):

workflow.fulfill(‘FulfillmentSystem’)

Run distributed tasks with retry logic

notify_warehouse.delay(order_id)

notify_customer.delay(order_id)

Update the order status

order = Order.query.get(order_id)

order.state = ‘Completed’

Commit the transaction if all steps succeed

db.session.commit()

return jsonify({‘status’: ‘success’, ‘new_state’: ‘Completed’}), 200

except Exception as e:

Roll back the transaction if any step fails

db.session.rollback()

return jsonify({‘status’: ‘error’, ‘message’: str(e)}), 400

Step 2: Handling Partial Failures with Compensation Transactions

In distributed systems, some tasks may succeed while others fail. In such cases, you may need to implement compensation transactions---a process of reversing completed steps if the overall workflow cannot proceed.

Example: Implementing Compensation Logic:

def reverse_order(order_id):

order = Order.query.get(order_id)

order.state = ‘Pending’

db.session.commit()

print(f”Order {order_id} has been rolled back to ‘Pending’ state.”)

You can invoke the reverse_order function if certain tasks in the workflow fail after partial completion.

6. Practical Example: Fault-Tolerant Order Fulfillment System

Let’s implement a full fault-tolerant order fulfillment system using the strategies we’ve discussed: task retries with Celery, database transactions, and compensation transactions.

DSL for Fault-Tolerant Workflow:

workflow OrderProcessing {

state Pending {

transition approve -> Approved by Manager;

transition reject -> Rejected by Manager;

}

state Approved {

concurrent task notifyWarehouse, notifyCustomer -> Completed retry 3 times, timeout 5 seconds;

}

state Completed {

event shipmentConfirmed -> Shipped by LogisticsSystem;

}

state Shipped {

// End of workflow

}

}

Step 1: Generated Workflow Code with Fault Tolerance:

from celery import Celery

class OrderProcessingWorkflow:

states =

′Pending′,′Approved′,′Rejected′,′Completed′,′Shipped′,'Pending', 'Approved', 'Rejected', 'Completed', 'Shipped',

def init(self):

self.state = ‘Pending’

def approve(self, actor):

if self.state == ‘Pending’ and actor == ‘Manager’:

self.state = ‘Approved’

else:

raise ValueError(“Invalid transition or actor”)

def fulfill(self, actor):

if self.state == ‘Approved’ and actor == ‘FulfillmentSystem’:

notify_warehouse.delay

(order_id)

notify_customer.delay(order_id)

self.state = ‘Completed’

else:

raise ValueError(“Invalid transition or actor”)

def shipment_confirmed(self, order_id):

handle_shipment_confirmation.delay(order_id)

self.state = ‘Shipped’

Step 2: Full Workflow with Fault Tolerance and Database Transactions:

@app.route(‘/order/int:order_id/fulfill’, methods=

′POST′'POST'

)

def fulfill_order(order_id):

workflow = OrderProcessingWorkflow()

try:

Start a transaction

with db.session.begin(subtransactions=True):

workflow.fulfill(‘FulfillmentSystem’)

Run tasks with retry logic

notify_warehouse.delay(order_id)

notify_customer.delay(order_id)

Update the order status

order = Order.query.get(order_id)

order.state = ‘Completed’

Commit if all tasks succeed

db.session.commit()

return jsonify({‘status’: ‘success’, ‘new_state’: ‘Completed’}), 200

except Exception as e:

Rollback if any task fails

db.session.rollback()

return jsonify({‘status’: ‘error’, ‘message’: str(e)}), 400

In this example, the workflow will automatically retry failed tasks and roll back the transaction if the tasks cannot complete successfully.

7. Conclusion and Next Steps

In this lesson, we’ve:

  • Explored strategies for fault tolerance in distributed systems.

  • Implemented retry mechanisms in Celery for task failure handling.

  • Used database transactions and rollbacks to ensure atomicity in workflows.

  • Implemented compensation transactions to handle partial failures.

This approach ensures that your system can gracefully recover from failures while maintaining data integrity and preventing partial updates.

Practical Exercise

Task: Extend the DSL to support circuit breakers and fallback actions. Implement circuit breakers that temporarily halt task execution after repeated failures and provide fallback logic for workflows.

Hints:

  • Use a circuit breaker pattern to monitor task failures and open the circuit if failures exceed a threshold.

  • Implement fallback logic (e.g., using a backup service or alternative path) in case of repeated failures.

Example DSL:

workflow OrderProcessing {

state Pending {

transition approve -> Approved by Manager;

transition reject -> Rejected by Manager;

}

state Approved {

concurrent task notifyWarehouse, notifyCustomer -> Completed retry 3 times, timeout 5 seconds;

circuitBreaker threshold 5;

fallback useBackupWarehouse if failureCount exceeds 5;

}

}

Looking Ahead

In the next lesson, we’ll explore monitoring, logging, and observability in distributed systems. We’ll focus on tracking system performance, monitoring failures, and gaining visibility into the behavior of your workflows.

Feel free to reach out if you have any questions. See you in the next lesson!