-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Remove Kafka between food ordering UI and order workflow (#98)
* Remove Kafka event handler as source of order workflow and clean up for conference presentation * Fix readme
- Loading branch information
Showing
29 changed files
with
612 additions
and
669 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
FROM node:18-alpine | ||
|
||
# dumb-init helps handling SIGTERM and SIGINT correctly | ||
RUN apk add dumb-init | ||
|
||
WORKDIR /usr/src/app | ||
|
||
# copy package.json and package-lock.json separately to cache dependencies | ||
COPY package*.json . | ||
RUN npm install | ||
|
||
COPY --chown=node:node .. . | ||
|
||
RUN npm run build | ||
|
||
RUN npm prune --production | ||
ENV NODE_ENV production | ||
|
||
EXPOSE 9081 | ||
USER node | ||
ENTRYPOINT ["dumb-init", "node", "./dist/delivery-app/app.js"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
91 changes: 91 additions & 0 deletions
91
typescript/food-ordering/app/src/delivery-app/delivery_manager.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/* | ||
* Copyright (c) 2024 - Restate Software, Inc., Restate GmbH | ||
* | ||
* This file is part of the Restate examples, | ||
* which is released under the MIT license. | ||
* | ||
* You can find a copy of the license in the file LICENSE | ||
* in the root directory of this repository or package or at | ||
* https://github.com/restatedev/examples/ | ||
*/ | ||
|
||
import * as restate from "@restatedev/restate-sdk"; | ||
import * as driverDigitalTwin from "./driver_digital_twin"; | ||
import * as driverDeliveryMatcher from "./driver_delivery_matcher"; | ||
import * as geo from "./utils/geo"; | ||
import {DEMO_REGION, Location, DeliveryInformation, OrderAndPromise, Status} from "./types/types"; | ||
import * as orderstatus from "../order-app/order_status_service"; | ||
|
||
/** | ||
* Manages the delivery of the order to the customer. Keyed by the order ID (similar to the | ||
* OrderService and OrderStatusService). | ||
*/ | ||
export const service: restate.ServiceApi<typeof router> = { path: "delivery-manager" }; | ||
|
||
const DELIVERY_INFO = "delivery-info"; | ||
|
||
export const router = restate.keyedRouter({ | ||
// Called by the OrderService when a new order has been prepared and needs to be delivered. | ||
start: async (ctx: restate.RpcContext, deliveryId: string, { order, promise }: OrderAndPromise) => { | ||
const [restaurantLocation, customerLocation] = await ctx.sideEffect(async () => [geo.randomLocation(), geo.randomLocation()]); | ||
|
||
// Store the delivery information in Restate's state store | ||
const deliveryInfo: DeliveryInformation = { | ||
orderId: order.id, | ||
orderPromise: promise, | ||
restaurantId: order.restaurantId, | ||
restaurantLocation, | ||
customerLocation, | ||
orderPickedUp: false | ||
} | ||
ctx.set(DELIVERY_INFO, deliveryInfo); | ||
|
||
// Acquire a driver | ||
const driverPromise = ctx.awakeable<string>(); | ||
ctx.send(driverDeliveryMatcher.service).requestDriverForDelivery(DEMO_REGION, { promiseId: driverPromise.id }); | ||
// Wait until the driver pool service has located a driver | ||
// This awakeable gets resolved either immediately when there is a pending delivery | ||
// or later, when a new delivery comes in. | ||
const driverId = await driverPromise.promise; | ||
|
||
// Assign the driver to the job | ||
await ctx.rpc(driverDigitalTwin.service).assignDeliveryJob(driverId, { | ||
deliveryId, | ||
restaurantId: order.restaurantId, | ||
restaurantLocation: deliveryInfo.restaurantLocation, | ||
customerLocation: deliveryInfo.customerLocation | ||
}); | ||
|
||
ctx.send(orderstatus.service).setStatus(order.id, Status.WAITING_FOR_DRIVER); | ||
}, | ||
|
||
// called by the DriverService.NotifyDeliveryPickup when the driver has arrived at the restaurant. | ||
notifyDeliveryPickup: async (ctx: restate.RpcContext, _deliveryId: string) => { | ||
const delivery = (await ctx.get<DeliveryInformation>(DELIVERY_INFO))!; | ||
delivery.orderPickedUp = true; | ||
ctx.set(DELIVERY_INFO, delivery); | ||
|
||
ctx.send(orderstatus.service).setStatus(delivery.orderId, Status.IN_DELIVERY); | ||
}, | ||
|
||
// Called by the DriverService.NotifyDeliveryDelivered when the driver has delivered the order to the customer. | ||
notifyDeliveryDelivered: async (ctx: restate.RpcContext, _deliveryId: string) => { | ||
const delivery = (await ctx.get<DeliveryInformation>(DELIVERY_INFO))!; | ||
ctx.clear(DELIVERY_INFO); | ||
|
||
// Notify the OrderService that the delivery has been completed | ||
ctx.resolveAwakeable(delivery.orderPromise, null); | ||
}, | ||
|
||
// Called by DriverDigitalTwin.HandleDriverLocationUpdateEvent() when the driver moved to new location. | ||
handleDriverLocationUpdate: async (ctx: restate.RpcContext, _deliveryId: string, location: Location) => { | ||
const delivery = (await ctx.get<DeliveryInformation>(DELIVERY_INFO))!; | ||
|
||
// Parse the new location, and calculate the ETA of the delivery to the customer | ||
const eta = delivery.orderPickedUp | ||
? geo.calculateEtaMillis(location, delivery.customerLocation) | ||
: geo.calculateEtaMillis(location, delivery.restaurantLocation) | ||
+ geo.calculateEtaMillis(delivery.restaurantLocation, delivery.customerLocation); | ||
ctx.send(orderstatus.service).setETA(delivery.orderId, eta); | ||
}, | ||
}); |
63 changes: 63 additions & 0 deletions
63
typescript/food-ordering/app/src/delivery-app/driver_delivery_matcher.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
/* | ||
* Copyright (c) 2024 - Restate Software, Inc., Restate GmbH | ||
* | ||
* This file is part of the Restate examples, | ||
* which is released under the MIT license. | ||
* | ||
* You can find a copy of the license in the file LICENSE | ||
* in the root directory of this repository or package or at | ||
* https://github.com/restatedev/examples/ | ||
*/ | ||
|
||
import * as restate from "@restatedev/restate-sdk"; | ||
import { PendingDelivery } from "./types/types"; | ||
|
||
/** | ||
* Links available drivers to delivery requests Keyed by the region. Each region has a pool of | ||
* available drivers and orders waiting for a driver. This service is responsible for tracking and | ||
* matching the two. | ||
*/ | ||
export const service : restate.ServiceApi<typeof router> = { path: "driver-delivery-matcher" }; | ||
|
||
const PENDING_DELIVERIES = "pending-deliveries"; | ||
const AVAILABLE_DRIVERS = "available-drivers"; | ||
|
||
export const router = restate.keyedRouter({ | ||
setDriverAvailable: async (ctx: restate.RpcContext, _region: string, driverId: string) => { | ||
// if we have deliveries already waiting, assign those | ||
const pendingDeliveries = await ctx.get<PendingDelivery[]>(PENDING_DELIVERIES); | ||
if (pendingDeliveries && pendingDeliveries.length > 0) { | ||
const nextDelivery = pendingDeliveries.shift()!; | ||
ctx.set(PENDING_DELIVERIES, pendingDeliveries); | ||
|
||
// Notify that delivery is ongoing | ||
ctx.resolveAwakeable(nextDelivery.promiseId, driverId); | ||
return; | ||
} | ||
|
||
// otherwise remember driver as available | ||
const availableDrivers = (await ctx.get<string[]>(AVAILABLE_DRIVERS)) ?? []; | ||
availableDrivers.push(driverId); | ||
ctx.set(AVAILABLE_DRIVERS, availableDrivers); | ||
}, | ||
|
||
// Called when a new delivery gets scheduled. | ||
requestDriverForDelivery: async (ctx: restate.RpcContext, _region: string, request: PendingDelivery) => { | ||
// if a driver is available, assign the delivery right away | ||
const availableDrivers = (await ctx.get<string[]>(AVAILABLE_DRIVERS)); | ||
if (availableDrivers && availableDrivers.length > 0) { | ||
// Remove driver from the pool | ||
const nextAvailableDriver = availableDrivers.shift()!; | ||
ctx.set(AVAILABLE_DRIVERS, availableDrivers); | ||
|
||
// Notify that delivery is ongoing | ||
ctx.resolveAwakeable(request.promiseId, nextAvailableDriver); | ||
return; | ||
} | ||
|
||
// otherwise store the delivery request until a new driver becomes available | ||
const pendingDeliveries = (await ctx.get<PendingDelivery[]>(PENDING_DELIVERIES)) ?? []; | ||
pendingDeliveries.push(request); | ||
ctx.set(PENDING_DELIVERIES, pendingDeliveries); | ||
}, | ||
}) |
88 changes: 88 additions & 0 deletions
88
typescript/food-ordering/app/src/delivery-app/driver_digital_twin.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
/* | ||
* Copyright (c) 2024 - Restate Software, Inc., Restate GmbH | ||
* | ||
* This file is part of the Restate examples, | ||
* which is released under the MIT license. | ||
* | ||
* You can find a copy of the license in the file LICENSE | ||
* in the root directory of this repository or package or at | ||
* https://github.com/restatedev/examples/ | ||
*/ | ||
|
||
import * as restate from "@restatedev/restate-sdk"; | ||
import * as driverDeliveryMatcher from "./driver_delivery_matcher"; | ||
import * as deliveryManager from "./delivery_manager"; | ||
import {DeliveryRequest, DriverStatus, Location} from "./types/types"; | ||
|
||
/** | ||
* Digital twin for the driver. Represents a driver and his status, assigned delivery, and location. | ||
* Keyed by driver ID. The actual driver would have an application (mocked by Driver Mobile App Simulator) that | ||
* calls this service. | ||
*/ | ||
export const service: restate.ServiceApi<typeof router> = {path: "driver-digital-twin"}; | ||
|
||
const DRIVER_STATUS = "driver-status"; | ||
const ASSIGNED_DELIVERY = "assigned-delivery"; | ||
const DRIVER_LOCATION = "driver-location"; | ||
|
||
export const router = restate.keyedRouter({ | ||
// Called by driver's mobile app at start of workday or end of delivery | ||
setDriverAvailable: async (ctx: restate.RpcContext, driverId: string, region: string) => { | ||
await checkIfDriverInExpectedState(DriverStatus.IDLE, ctx); | ||
|
||
ctx.set(DRIVER_STATUS, DriverStatus.WAITING_FOR_WORK); | ||
ctx.send(driverDeliveryMatcher.service).setDriverAvailable(region, driverId); | ||
}, | ||
|
||
// Gets polled by the driver's mobile app at regular intervals to check for assignments. | ||
getAssignedDelivery: async (ctx: restate.RpcContext) => ctx.get<DeliveryRequest>(ASSIGNED_DELIVERY), | ||
|
||
// Gets called by the delivery manager when this driver was assigned to do the delivery. | ||
assignDeliveryJob: async (ctx: restate.RpcContext, _driverId: string, deliveryRequest: DeliveryRequest) => { | ||
await checkIfDriverInExpectedState(DriverStatus.WAITING_FOR_WORK, ctx); | ||
|
||
ctx.set(DRIVER_STATUS, DriverStatus.DELIVERING); | ||
ctx.set(ASSIGNED_DELIVERY, deliveryRequest); | ||
|
||
const currentLocation = await ctx.get<Location>(DRIVER_LOCATION); | ||
if (currentLocation) { | ||
ctx.send(deliveryManager.service).handleDriverLocationUpdate(deliveryRequest.deliveryId, currentLocation); | ||
} | ||
}, | ||
|
||
// Called by driver's mobile app at pickup from the restaurant. | ||
notifyDeliveryPickUp: async (ctx: restate.RpcContext, _driverId: string) => { | ||
await checkIfDriverInExpectedState(DriverStatus.DELIVERING, ctx); | ||
const assignedDelivery = (await ctx.get<DeliveryRequest>(ASSIGNED_DELIVERY))!; | ||
ctx.send(deliveryManager.service).notifyDeliveryPickup(assignedDelivery.deliveryId); | ||
}, | ||
|
||
// Called by driver's mobile app after delivery success. | ||
notifyDeliveryDelivered: async (ctx: restate.RpcContext, _driverId: string) => { | ||
await checkIfDriverInExpectedState(DriverStatus.DELIVERING, ctx); | ||
|
||
const assignedDelivery = (await ctx.get<DeliveryRequest>(ASSIGNED_DELIVERY))!; | ||
ctx.clear(ASSIGNED_DELIVERY); | ||
ctx.send(deliveryManager.service).notifyDeliveryDelivered(assignedDelivery.deliveryId); | ||
ctx.set(DRIVER_STATUS, DriverStatus.IDLE); | ||
}, | ||
|
||
// Called by the driver's mobile app when he has moved to a new location. | ||
handleDriverLocationUpdateEvent: restate.keyedEventHandler(async (ctx: restate.RpcContext, event: restate.Event) => { | ||
const location = event.json<Location>(); | ||
ctx.set(DRIVER_LOCATION, location); | ||
const assignedDelivery = await ctx.get<DeliveryRequest>(ASSIGNED_DELIVERY); | ||
if (assignedDelivery) { | ||
ctx.send(deliveryManager.service).handleDriverLocationUpdate(assignedDelivery.deliveryId, location); | ||
} | ||
}) | ||
}) | ||
|
||
|
||
async function checkIfDriverInExpectedState(expectedStatus: DriverStatus, ctx: restate.RpcContext): Promise<void> { | ||
const currentStatus = (await ctx.get<DriverStatus>(DRIVER_STATUS)) ?? DriverStatus.IDLE; | ||
|
||
if (currentStatus !== expectedStatus) { | ||
throw new restate.TerminalError(`Driver status wrong. Expected ${expectedStatus} but was ${currentStatus}`); | ||
} | ||
} |
Oops, something went wrong.