Skip to content

Commit

Permalink
Nextjs background Job template (#687)
Browse files Browse the repository at this point in the history
Changed the nextjs template to be the "start background job" "Crash
application app"
  • Loading branch information
manojdbos authored Dec 27, 2024
1 parent 85d1ceb commit 8f0652b
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 12 deletions.
29 changes: 19 additions & 10 deletions packages/create/templates/hello-nextjs/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# DBOS Hello
# DBOS Background Job

This is a [DBOS app](https://docs.dbos.dev/) bootstrapped with `npx @dbos-inc/create`, using [Express.js](https://expressjs.com/) and [Knex](https://docs.dbos.dev/tutorials/using-knex) to interact with postgres.

Expand Down Expand Up @@ -40,12 +40,22 @@ npm run start

To see that it's working, visit this URL in your browser: [`http://localhost:3000/`](http://localhost:3000/).

Click on the "Run DBOS Workflow" button.
Click on the "Start Background Job" button.

You should get this message: `Hello, dbos! You have been greeted 1 times.`
Each time you refresh the page, the counter should go up by one!
You should this message: `Your background task has completed step 0 of 9.`
As the background job completes steps, the step counter should go up by one!

Congratulations! You just launched a DBOS application.
Click on "Crash the application" button.

The step counter stops increasing.
On the command line, you will see that the application has stopped.
Start the application again.
```bash
npm run start
```
In the browser, you will see that the execution of steps resumes where it left off and eventually completes.

Congratulations! You just run a DBOS application.

## The application

Expand All @@ -57,9 +67,9 @@ if (process.env.NEXT_PHASE !== "phase-production-build") {
await DBOS.launch();
}
```
- The workflow is called by the POST method in app/greetings/route.ts.
- The workflow for the background job is called by the called the GET method in app/tasks/route.ts.

- The POST is called by the component in src/components/callDBOSWorkflow.tsx. It calls the route /greetings.
- The GET is called by the component in src/components/BackGroundTask.tsx. It calls the route /tasks.

- The component is called from the main UI page.tsx.

Expand All @@ -72,7 +82,7 @@ To add more functionality to this application, modify `src/operations.ts`. If yo

## Running in DBOS Cloud

To deploy this app to DBOS Cloud, first install the DBOS Cloud CLI (example with [npm](https://www.npmjs.com/)):
To deploy this app to DBOS Cloud, first install the DBOS Cloud CLI (example with [npm](https://www.npmjs.com/)):

```shell
npm i -g @dbos-inc/dbos-cloud
Expand All @@ -87,5 +97,4 @@ dbos-cloud app deploy
## Next Steps

- For a detailed tutorial, check out our [programming quickstart](https://docs.dbos.dev/getting-started/quickstart-programming).
- To learn more about DBOS, take a look at [our documentation](https://docs.dbos.dev/) or our [source code](https://github.com/dbos-inc/dbos-transact).

- To learn more about DBOS, take a look at [our documentation](https://docs.dbos.dev/) or our [source code](https://github.com/dbos-inc/dbos-transact).
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,25 @@ class dbosWorkflowClass {
const greeting = `Hello! You have been greeted ${greet_count} times.`;
return greeting;
}

@DBOS.transaction()
static async backgroundTaskStep(i : number) {
DBOS.logger.info(`Completed step ${i}`);
}

@DBOS.workflow()
static async backgroundTask(i: number) {
DBOS.logger.info("Hello from background task!");
for (let j = 1; j <= i; j++) {
await dbosWorkflowClass.backgroundTaskStep(j);
DBOS.logger.info("Sleeping for 2 seconds");
await DBOS.sleepSeconds(2);
await DBOS.setEvent("steps_event", j)
}
DBOS.logger.info("Background task complete!");
}


}

// Launch the DBOS runtime
Expand All @@ -34,4 +53,9 @@ if (process.env.NEXT_PHASE !== "phase-production-build") {
export async function dbosWorkflow(userName: string) {
DBOS.logger.info("Hello from DBOS!");
return await dbosWorkflowClass.helloDBOS(userName);
}

export async function dbosBackgroundTask(workflowID: string) {
DBOS.logger.info("Hello from DBOS!");
return DBOS.startWorkflow(dbosWorkflowClass, {workflowID: workflowID}).backgroundTask(10);
}
8 changes: 8 additions & 0 deletions packages/create/templates/hello-nextjs/src/app/crash/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

export async function GET(request: Request) {

console.log("Received request Crashing the app");

process.exit(1);

}
12 changes: 10 additions & 2 deletions packages/create/templates/hello-nextjs/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import Image from "next/image";
import CallDBOSWorkflow from "../components/client/callDBOSWorkflow";
import BackGroundTask from "@/components/client/BackGroundTask";

export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">


<h1 className="text-xl font-semibold mb-4">Welcome to DBOS!</h1>


<p className="mb-4">
DBOS helps you build applications that are <strong>resilient to any failure</strong>&mdash;no matter how many times you crash this app, your background task will always recover from its last completed step in about ten seconds.
</p>
<div>
<CallDBOSWorkflow wfResult=""/>
<BackGroundTask />
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NextResponse } from "next/server";
import { DBOS } from "@dbos-inc/dbos-sdk";

export async function GET(request: Request, { params }: { params: Promise<{ slug: string }> }) {

const taskId = (await params).slug;
DBOS.logger.info(`Received request to check on taskId: ${taskId}`);

let step = await DBOS.getEvent(taskId, "steps_event");

DBOS.logger.info(`For taskId: ${taskId} we are done with ${step} steps`);

return NextResponse.json({ "stepsCompleted": step});

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NextResponse } from "next/server";
import { DBOS } from "@dbos-inc/dbos-sdk";
import { dbosBackgroundTask } from "@/actions/dbosWorkflow";

export async function GET(request: Request, { params }: { params: Promise<{ slug: string }> }) {

const taskId = (await params).slug;

DBOS.logger.info(`Received request to start background task taskId: ${taskId}`);

DBOS.logger.info(`Started background task taskId: ${taskId}`);

await dbosBackgroundTask(taskId)

return NextResponse.json({ message: "Background task started" });

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
"use client";

import { useState, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { dbosBackgroundTask } from "@/actions/dbosWorkflow";
import { Suspense } from 'react'

let intervalInitialized = false;

function generateRandomString(): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const array = new Uint8Array(6);
crypto.getRandomValues(array); // Fills the array with cryptographically random values

return Array.from(array)
.map(x => chars[x % chars.length])
.join('');
}

function BackGroundTask() {
const [isRunning, setIsRunning] = useState(false);
const [currentStep, setCurrentStep] = useState(0);
const [taskId, setTaskid] = useState("");
const [isReconnecting, setIsReconnecting] = useState(false);

const router = useRouter();
const searchParams = useSearchParams();

const startBackgroundJob = async () => {
setIsRunning(true);

setCurrentStep(0);

let task = taskId
if (taskId === "") {
task = generateRandomString();
setTaskid(task);
updateQueryParam("id", task);
}

// start the background job
try {
await fetch(`/tasks/${task}`, { method: "GET" });
} catch (error) {
console.error("Failed to start job", error);
setIsRunning(false);
}
};

const crashApp = async () => {

if(!isRunning) {
console.log("Not running, nothing to crash");
return;
}

setIsRunning(false);

console.log("Crashing the application");

// stop the background job
try {
await fetch("/crash", { method: "GET" });
} catch (error) {
console.error("Failed to start job", error);

}

};

// Update the URL query parameter
const updateQueryParam = (key: string, value: string) => {
const params = new URLSearchParams(searchParams.toString());
params.set(key, value);
const newUrl = `${window.location.pathname}?${params.toString()}`;
router.replace(newUrl);
};

// Remove the `id` query parameter from the URL
const clearQueryParam = (key: string) => {
const params = new URLSearchParams(searchParams.toString());
params.delete(key);
const newUrl = `${window.location.pathname}?${params.toString()}`;
router.replace(newUrl);
};

// fetch the current progress
const fetchProgress = async () => {
try {

if (taskId === "") {
console.log("No task to monitor");
return;
}

const response = await fetch(`/step/${taskId}`, { method: "GET" });
if (!response.ok) {
console.error("Failed to fetch job progress", response.statusText);
setIsReconnecting(true);
return;
}
setIsReconnecting(false);

const data = await response.json();

console.log("Step completed", data.stepsCompleted);

if (data.stepsCompleted) {
setIsRunning(true);
setCurrentStep(data.stepsCompleted);


if (data.stepsCompleted === 10) {
clearQueryParam("id");
setIsRunning(false);
setTaskid("");
setCurrentStep(0);

}
}
} catch (error) {
console.error("Failed to fetch job progress", error);
setIsRunning(false);
setTaskid("");
setIsReconnecting(true);
}
};

// Polling the progress every 2 seconds while the job is running
useEffect(() => {
const idFromUrl = searchParams.get("id");
if (idFromUrl) {
setTaskid(idFromUrl);
setIsRunning(true); // Assume the job is already running if there's an ID
}

if (!intervalInitialized) {
const interval = setInterval(fetchProgress, 2000);
intervalInitialized = true;
return () => {
clearInterval(interval);
intervalInitialized = false;
};
}
}, [searchParams]);


return (
<div>
<div className="flex flex-row gap-2">
<button onClick={startBackgroundJob} disabled={isRunning} className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
{isRunning ? "Job in Progress..." : "Start Background Job"}
</button>
<button onClick={crashApp} disabled={!isRunning} className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600">
{ isRunning ? "Crash the application" : "Not Running"}
</button>
</div>

<p>
{currentStep < 10
? `Your background task has completed step ${currentStep} of 10.`
: "Background task completed successfully!"}
</p>
<p>
{isReconnecting ? "Reconnecting..." : ""}
</p>


</div>
);

}

const WrappedBackgroundJobComponent = () => (
<Suspense fallback={<p>Loading...</p>}>
<BackGroundTask />
</Suspense>
);

export default WrappedBackgroundJobComponent;

0 comments on commit 8f0652b

Please sign in to comment.