Skip to content

Commit

Permalink
Add the crash button and instructions for a demo. (#165)
Browse files Browse the repository at this point in the history
We improve bank by adding a red `Crash!` button to it :) The button
stops the process (like widget store). Combined with a sleep loop in
transaction recovery, this makes for a compelling demo. Adde readme for
those steps.
  • Loading branch information
apoliakov authored Jul 5, 2024
1 parent b884fab commit 472115b
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 12 deletions.
37 changes: 37 additions & 0 deletions bank/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,43 @@ npm start

DBOS Cloud comes with a [monitoring dashboard](https://docs.dbos.dev/cloud-tutorials/monitoring-dashboard) automatically. To find the URL for your dashboard, execute `npx dbos-cloud dashboard url`. The dashboard includes execution traces.

## A Demo of Workflow Recovery

We can simulate a catastrophic failure during a transfer. Namely the following case:
1. we try to send a transfer from Bank A to Bank B
1. we make the transfer fail, as if bank B went offline
2. we crash Bank A before it has a chance to recover and undo the transfer

DBOS workflows are guaranteed to continue where they left off. This means that when Bank A is restarted, it will continue undoing the transfer. Shortly after restart, Bank A returns to a consistent state with the funds back in the source account.

To replicate this, perform the following:
1. in `withdrawWorkflow` (bank-backend/src/workflows/txnhistory.workflows.ts) uncomment the sleep block like so:
```ts
if (!remoteRes) {
// Sleep for 10 seconds before undoing the transaction
for (let i = 0; i < 10; i++) {
ctxt.logger.info("Sleeping")
await ctxt.sleepms(1000)
}

// Undo withdrawal with a deposit.
const undoRes = await ctxt.invoke(BankTransactionHistory).updateAcctTransactionFunc(data.fromAccountId, data, true, result);
if (undoRes !== result) {
throw new Error(`Mismatch: Original txnId: ${result}, undo txnId: ${undoRes}`);
}
throw new Error("Failed to deposit to remote bank; transaction reversed");
}
```
2. Restart or redeploy Bank A with this change.
3. Stop Bank B: `CTRL+C` the app if running locally or something like `npx dbos-cloud app delete bank_b` if in the cloud.

This demo is time sensitive as you'll have a 10-second sleep window to crash Bank A. Adjust the above `sleep` loop if you nee more time. Read these steps ahead of time to get a sense for what to do:
1. Initiate a withdrawal from Bank A to Bank B
2. Because of the above `sleep` loop, you won't see the effect. You can quickly close the "Withdraw" window and refresh the browser to see that the funds left the account. Now, quickly press the red "Crash!" button.
3. If running in DBOS cloud, wait a few seconds. If you are running locally, restart Bank A by hand.
4. After restarting the app, log in again to `https://localhost:8089`. Observe the funds returning to the sender account.
5. In the log for the app, you should see the failed transfer. The 10 `Sleeping` statements, interrupted by a crash, then restart, workflow recovery and finally the "transaction reversed" error message.

## Further Reading
To get started with DBOS Transact, check out the [quickstart](https://docs.dbos.dev/getting-started/quickstart) and [docs](https://docs.dbos.dev/).

Expand Down
11 changes: 4 additions & 7 deletions bank/bank-backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion bank/bank-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"license": "ISC",
"private": true,
"dependencies": {
"@dbos-inc/dbos-sdk": "^1.15.9",
"@dbos-inc/dbos-sdk": "^1.16.12",
"@koa/bodyparser": "^5.0.0",
"@koa/cors": "^5.0.0",
"@koa/router": "^12.0.0",
Expand Down
10 changes: 10 additions & 0 deletions bank/bank-backend/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ export class BankEndpoints {
}
}

// For demo purposes
export class CrashEndpoint {
@GetApi('/crash_application')
static async crashApplication(_ctx: HandlerContext) {
// For testing and demo purposes :)
process.exit(1);
return Promise.resolve();
}
}

// Helper functions to convert to the correct data types.
// Especially convert the bigint.
export function convertTransactionHistory(data: TransactionHistory): TransactionHistory {
Expand Down
19 changes: 16 additions & 3 deletions bank/bank-backend/src/workflows/txnhistory.workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,6 @@ export class BankTransactionHistory {
static async withdrawWorkflow(ctxt: WorkflowContext, data: TransactionHistory) {
// Withdraw first.
const result = await ctxt.invoke(BankTransactionHistory).updateAcctTransactionFunc(data.fromAccountId, data, false);

// Then, contact remote DB to deposit.
if (data.toLocation && !(data.toLocation === "cash") && !data.toLocation.startsWith(REMOTEDB_PREFIX)) {
ctxt.logger.info("Deposit to another DB: " + data.toLocation + ", account: " + data.toAccountId);
Expand All @@ -240,12 +239,26 @@ export class BankTransactionHistory {
};
const remoteRes: boolean = await ctxt.invoke(BankTransactionHistory).remoteTransferComm(remoteUrl, thReq as TransactionHistory, ctxt.workflowUUID + '-deposit');
if (!remoteRes) {
// Undo transaction is a deposit.

///////////////////////////////
// Example sleep Window. For a reliability test, uncomment the below
// Then, start a transfer to a nonexistent bank, (i.e. stop bank b).
// Wait for app to go into sleep and then crash it. The DBOS workflow
// recovery will ensure the undo transaction below is executed when
// the app restarts
//
// for (let i = 0; i < 10; i++) {
// ctxt.logger.info("Sleeping")
// await ctxt.sleepms(1000)
// }
///////////////////////////////

// Undo withdrawal with a deposit.
const undoRes = await ctxt.invoke(BankTransactionHistory).updateAcctTransactionFunc(data.fromAccountId, data, true, result);
if (undoRes !== result) {
throw new Error(`Mismatch: Original txnId: ${result}, undo txnId: ${undoRes}`);
}
throw new Error("Failed to deposit to remote bank.");
throw new Error("Failed to deposit to remote bank; transaction reversed");
}
} else {
ctxt.logger.info("Deposit to: " + data.fromLocation);
Expand Down
7 changes: 6 additions & 1 deletion bank/bank-frontend/src/app/bank.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { OwnerNameDialogComponent } from './owner-name-dialog/owner-name-dialog.
<button class="button-grey" (click)="getMsg()" type="submit">New Greeting Message</button>
<button class="button-green" (click)="createNewAccount()" type="submit">Create a New Account</button>
<button class="button" (click)="getAccounts()" type="submit">Refresh Accounts</button>
<button class="button-red" (click)="crashApp()" type="submit">Crash!</button>
</div>
<table class="table table-striped">
Expand Down Expand Up @@ -431,4 +431,9 @@ export class BankComponent {
error: (err: any) => { this.bankmsg = 'Failed to transfer! '; }
});
}

//In response to the "Crash" button - for demo purposes
crashApp(){
this._service.getResource(this.bankUrl + "/crash_application").subscribe()
}
}
12 changes: 12 additions & 0 deletions bank/bank-frontend/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@
position: relative;
}

.button-red {
margin: 10px;
padding: 15px 20px;
background-color: #ff1111;
color: white;
border: none;
cursor: pointer;
font-size: 16px;
border-radius: 10px; /* Rounded corners */
position: relative;
}

.button-grey {
margin: 10px;
padding: 15px 20px;
Expand Down

0 comments on commit 472115b

Please sign in to comment.