Real-time Map displays real-time positions of public transport vehicles in Helsinki. It's a showcase for Proto.Actor - an ultra-fast distributed actors solution for Go, C#, and Java/Kotlin.
🔥 Check out the live demo! 🔥
The app features:
The goals of this app are:
Find more about Proto.Actor here.
Configure Mapbox:
Frontend\src\config.ts
.Start frontend (requires node.js 17.x):
cd Frontend
npm install
npm run serve
Start Backend:
cd Backend
dotnet run
The app is available on localhost:8080.
Also check out the proto.actor dashboard (alpha) by navigating to localhost:5000.
In order to deploy to Kubernetes and use the Kubernetes cluster provider, see Deploying to Kubernetes
To understand how this app works, it's highly recommended to understand the basics of the following technologies:
It would also be great, if you knew the basics the of actor model, Proto.Actor, and virtual actors (also called grains). If you don't you can try reading this document anyway: we'll try to explain the essential parts as we go.
Learn more about Proto.Actor here.
Learn more about virtual actors here.
Also, since this app aims to provide horizontal scalability, this document will assume, that we're running a cluster with two nodes.
One last note: this document is not a tutorial, but rather a documentation of this app. If you're learning Proto.Actor, you'll benefit the most by jumping between code and this document.
Since this app is all about tracking vehicles, we need to get their positions from somewhere. In this app, the positions are received from high-frequency vehicle positioning MQTT broker from Helsinki Region Transport. More info on data:
This data is licensed under © Helsinki Region Transport 2021, Creative Commons BY 4.0 International
In our app, Ingress
is a hosted service responsible for subscribing to Helsinki Region Transport MQTT server and handling vehicle position updates.
When creating a system with actors, is common to model real-world physical objects as actors. We'll start by modelling a vehicle since all the features depend on it. It will be implemented as a virtual actor (VehicleActor
). It will be responsible for handling events related to that vehicle and remembering its state, e.g. its current position and position history.
Quick info on virtual actors:
Vehicle
.Note: virtual actors are sometimes referred to as "grains" - terminology originating in the MS Orleans project.
The workflow looks like this:
Ingress
receives an event from Helsinki Region Transport MQTT server.Ingress
reads vehicle's ID and its new position from the event and sends it to a vehicle in question.VehicleActor
processes the message.Notice, that in the above diagram, vehicles (virtual actors) are distributed between two nodes, however, Ingress
is present in both of the nodes.
Let's consider the following feature: in this app, each vehicle belongs to an organization. Each organization has a specified list of geofences. In our case, a geofence is simply a circular area somewhere in Helsinki, e.g. airport, railway square, or downtown. Users should receive notifications when a vehicle enters or leaves a geofence (from that vehicle's organization).
For that purpose, we'll implement a virtual actor to model an organization (OrganizationActor
). When activated, OrganizationActor
will spawn (create) a child actor for each configured geofence (GeofenceActor
).
Quick info on child actors:
The workflow looks like this:
VehicleActor
receives a position update.VehicleActor
forwards position update to its organization.OrganizationActor
forwards position update to all its geofences.GeofenceActor
keeps track of which vehicles are already inside the zone. Thanks to this, GeofenceActor
can detect if a vehicle entered or left the geofence.So far we've only sent messages to and between actors. However, if we want to send notifications to actual users, we need to find a way to communicate between the actor system and the outside world. In this case, we'll use:
First we'll introduce the UserActor
. It models the user's ability to view positions of vehicles and geofencing notifications. UserActor
will be implemented as an actor (i.e. non-virtual actor).
Quick info on actors:
The workflow looks like this:
Position
and Notification
events being broadcasted through the Pub-Sub mechanism. It also makes sure to unsubscribe when stopping.Notification
message to all cluster members and available through PubSub. Same goes for Position
events broadcasted from vehicle actor.Notification
message, user actor will push it to the user via SignalR connection. It will do the same with Position
message provided it is within currently visible area on the map. The positions are sent in batches to improve performance.In the Realtime map sample we also showcase an optimization, which is possible thanks to the virtual actors and the pub-sub mechanism. Instead of broadcasting all positions to all users, we can limit the amount of positions sent to just the ones a particular user has interest in. For this purpose we overlay a "virtual grid" on top of the map.
Each cell in the map becomes a topic in the pub sub system. Topics are implemented as virtual actors so we don't need to create them in an explicit way. We can just start sending messages to them. In the example above we would have a topic for grid cell A1, B1, etc.
Depending on the size and position of the user's viewport, it covers some of the grid cells. This means the user actor needs to subscribe to topics corresponding to the covered grid cells. In the example above, the user is subscribed to topics A1, B1, A2 and B2. When the viewport moves to cover different set of grid cells, the topics corresponding to grid cells that are no longer relevant need to be unsubscribed.
On publishing side, the vehicle calculates what topic to publish to based on current position. Only the topic corresponding to the grid cell the vehicle is currently in receives the position.
This way we're removing a bottleneck in the system that would otherwise force us to send all the messages to all the users, which is not a scalable approach. You could argue, that if the viewport covers all the grid cells, the problem occurs anyway. This is true, but we could prevent users from doing so, for example by stopping the real time updates when the viewport is over some size threshold.
Let's consider the following feature: when a user requests it, we want to list all vehicles currently present in a selected organization's geofences.
This one is quite easy, as actors support request/response pattern.
The workflow looks like that:
OrganizationController
asks OrganizationActor
for the details.OrganizationActor
asks each of its geofences (GeofenceActor
) for a list of vehicles in these geofences.GeofenceActor
returns that information.OrganizationActor
combines that information into a response and returns it to OrganizationController
.OrganizationController
maps and returns the results to the user.Prerequisites:
The Realtime Map sample can be configured to use Kubernetes as cluster provider. The attached chart contains definition for the deployment. You will need to supply some additional configuration, so create a new values file, e.g. my-values.yaml
in the root directory of the sample. Example contents:
frontend:
config:
mapboxToken: SOME_TOKEN # provide your MapBox token here
backend:
config:
# since all backend pods need to share MQTT subscription,
# generate a new guid and provide it here (no dashes)
RealtimeMap__SharedSubscriptionGroupName: SOME_GUID
ProtoActor__PubSub__RedisConnectionString: realtimemap-redis-master:6379
ingress:
# specify localhost if on Docker Desktop,
# otherwise add a domain for the sample to your DNS and specify it here
host: localhost
Create the namespace and deploy the sample:
helm upgrade --install -f my-values.yml --namespace realtimemap --create-namespace realtimemap ./chart
NOTE: the chart creates a new role in the cluster with permissions to access Kubernetes API required for the cluster provider.
The above config will deploy the sample without TLS on the ingress, additional configuration is required to secure the connection.