Implementing the Saga Pattern for Distributed Transactions
Distributed systems often involve multiple independent services that need to coordinate to complete a single business transaction. Ensuring atomicity across these services is challenging. The Saga pattern offers a way to manage data consistency across microservices without relying on a distributed ACID transaction. This challenge will have you implement a basic Saga orchestrator to handle a sequence of operations and their compensation actions.
Problem Description
Your task is to implement a Saga pattern orchestrator in Python. A Saga is a sequence of local transactions. Each local transaction updates data within a single service and publishes a message or event to trigger the next local transaction in the saga. If a local transaction fails, the Saga executes a set of compensating transactions to undo the changes made by preceding local transactions.
You will need to create a SagaOrchestrator class that can:
- Register Steps: Allow users to register a sequence of steps. Each step should consist of a forward action (the operation to perform) and a compensation action (the operation to undo the forward action).
- Execute Saga: Run the registered steps sequentially. If all steps succeed, the saga completes successfully.
- Handle Failures: If any step fails, the orchestrator must execute the compensation actions for all successfully completed preceding steps in reverse order.
- Manage State: Keep track of which steps have been successfully executed and which have failed.
Key Requirements:
- The
SagaOrchestratorshould have a method likeadd_step(forward_action, compensation_action, step_name). - The
forward_actionandcompensation_actionshould be callable functions. - The orchestrator should have a method like
run(initial_data)to start the saga. - The
runmethod should return a boolean indicating success or failure. - If a
forward_actionraises an exception, the saga should be considered failed, and compensation should be triggered. - Compensation should only be triggered for steps that successfully completed their forward action.
- The
initial_datashould be passed to the firstforward_actionand the result of each step should be passed as input to the nextforward_action. - Compensation actions should also receive data relevant to the failed step, potentially the data that was passed to its corresponding forward action.
Expected Behavior:
- If all forward actions succeed, the saga completes successfully.
- If any forward action fails, the saga fails, and all previously completed forward actions are compensated in reverse order.
Edge Cases to Consider:
- A saga with no steps.
- A step failing immediately.
- A step failing after several preceding steps have already succeeded.
- A compensation action itself failing (for simplicity in this challenge, assume compensation actions succeed or are handled externally by retries, but be mindful of the concept).
Examples
Example 1: Successful Saga
Imagine a simple order placement saga:
- Reserve Inventory: Deduct items from stock.
- Process Payment: Charge the customer.
- Create Order: Generate an order record.
Input: initial_data = {"item_id": "A123", "quantity": 2, "user_id": "user1"}
Steps:
- Step 1: reserve_inventory (forward), release_inventory (compensation)
- Step 2: process_payment (forward), refund_payment (compensation)
- Step 3: create_order (forward), cancel_order (compensation)
Expected Output:
True (indicating successful completion)
Explanation:
All three forward actions (reserve_inventory, process_payment, create_order) execute successfully. No compensation is needed.
Example 2: Saga Failure and Compensation
Imagine the same order placement saga, but payment processing fails:
- Reserve Inventory: Deduct items from stock. (SUCCESS)
- Process Payment: Charge the customer. (FAILS)
- Create Order: Generate an order record. (NEVER REACHED)
Input: initial_data = {"item_id": "B456", "quantity": 1, "user_id": "user2"}
Steps:
- Step 1: reserve_inventory (forward), release_inventory (compensation)
- Step 2: process_payment (forward), refund_payment (compensation)
- Step 3: create_order (forward), cancel_order (compensation)
Assume process_payment raises an exception.
Expected Output:
False (indicating failure)
Explanation:
reserve_inventorysucceeds.process_paymentfails.- The saga orchestrator detects the failure.
- It then executes the compensation for
reserve_inventory(i.e.,release_inventory) in reverse order of completion. create_orderis never executed.
Example 3: Failure after Multiple Steps
Consider a slightly longer saga:
- Step A: Create User Profile.
- Step B: Send Welcome Email.
- Step C: Initialize User Preferences. (FAILS)
- Step D: Grant Initial Permissions. (NEVER REACHED)
Input: initial_data = {"username": "testuser", "email": "test@example.com"}
Steps:
- Step A: create_profile (forward), delete_profile (compensation)
- Step B: send_welcome_email (forward), resend_welcome_email (compensation) - simplified compensation
- Step C: init_preferences (forward), reset_preferences (compensation)
- Step D: grant_permissions (forward), revoke_permissions (compensation)
Assume init_preferences raises an exception.
Expected Output:
False (indicating failure)
Explanation:
create_profilesucceeds.send_welcome_emailsucceeds.init_preferencesfails.- The orchestrator triggers compensation:
- First, it compensates
send_welcome_email(usingresend_welcome_email- this highlights that compensation might not be a direct undo but a corrective action). - Then, it compensates
create_profile(usingdelete_profile).
- First, it compensates
grant_permissionsis never executed.
Constraints
- The
SagaOrchestratorshould be implemented as a single class. - The forward and compensation actions will be represented by Python functions.
- The maximum number of steps in a saga is 100.
- The input data passed between steps will be a dictionary.
- The solution should be efficient enough to handle up to 100 steps within a reasonable execution time (e.g., < 1 second for a purely in-memory simulation).
Notes
- This challenge focuses on the orchestration logic. You can simulate success and failure by having your placeholder functions either return a value or raise an exception.
- Consider how you will pass data between steps and how compensation actions will receive appropriate context.
- Think about how to manage the state of completed steps.
- For simplicity, assume that if a compensation action fails, it will raise an exception, and this exception will be handled by the orchestrator (though full error recovery for compensation failures is beyond the scope of this basic implementation).
- The concept of idempotency is crucial for Sagas, especially for compensation actions, but is not a primary focus of this specific challenge implementation.