Home

Awesome

Real-time communication for Antragsgrün

Antragsgrün is mostly a traditional Content Management System, designed to run on a wide variety of hosting environments, including shared hosting providers that provide no means of using websockets or long-running processes necessary for real-time communication. Therefore, interactive components like the speaking lists and the voting system use HTTP polling by default, which works for smaller events.

<img src="docs/Communication-flow.png" alt="1) PHP Provides signed JWT + WS URI to User. 2) User subscribes to topics via STOMP. 3) PHP sends generic events to RabbitMQ. 4) Live server retrieves generic events from RabbitMQ. 5) Live server sends user-specific events to user" width="350" align="right"> This Live Server is an optional component that can be deployed for larger setups and solves two issues of the traditional approach: 1) the latency of updates with which changes are propagated to web clients (which comes from the polling frequency), and 2) the load that this approach puts on the server, which tends to be an issue with larger voting sessions. <br> <br> Users are connecting to the Live Server via Websocket/STOMP when using an interactive part of Antragsgrün. Whenever an update to the internal state of the system happens, the main Antragsgrün system publishes a message with the new state to a message queue (RabbitMQ, at the moment), to be consumed by this Live Server. This processes the new state, transforms it to user-specific objects (which matches exactly the structure that the traditional polling HTTP endpoint would return) and actively sends it to the relevant connected users.

Authentication

ID Mapping

The IDs referred to by this service can be found at the following places in the central Antragsgrün system:

RabbitMQ Setup

The central Antragsgrün system publishes all its messages to one central exchange (by default: antragsgruen-exchange). Messages to all subdomains and consultations within a subdomain are published through that exchange, but are classified by a routing key pattern.

The following routing key patterns are fixed, while its associated queues can be configured:

In case messages cannot be processed by this live server, they are rejected and, through the antragsgruen-exchange-dead, end up in the dead letter queues antragsgruen-queue-speech-dead and antragsgruen-queue-user-dead.

Exposed Websocket STOMP Topics

Installing, Running, Configuration

Prerequisites

Before building the app, two steps have to be manually performed:

Running

This app requires a RabbitMQ to be running. The app can be compiled and started using:

./mvnw spring-boot:run

Hint: this is only meant for local development. On production, you want to secure the actuator endpoints, as they are not really protected in this basic setup (see application.yml).

Configuration via Environment Variables

One or multiple installations can be configured through environment variables. Each installation needs to have an ID and a public key. Mind that the numbering needs to be consecutive, starting with zero.

Environment Variable NameExplanation
ANTRAGSGRUEN_INSTALLATIONS_0_IDUnique ID of the Antragsgrün installation
ANTRAGSGRUEN_INSTALLATIONS_0_PUBLIC_KEYPublic RSA Key. Refer to the README in the Central System on how to generate one.
......

The following aspects can be configured through environment variables, especially valuable when deploying it via docker (compose):

Environment Variable NameDefault ValueExplanation
ANTRAGSGRUEN_WS_ORIGINShttp://localhostWeb origin to accept web requests from, e.g. http://*.antragsgruen.de. Multiple comma-separated patterns can be provided.
RABBITMQ_HOSTlocalhostRabbitMQ Hostname
RABBITMQ_VHOST/RabbitMQ VirtualHost
RABBITMQ_USERNAMEguestRabbitMQ Management Username
RABBITMQ_PASSWORDguestRabbitMQ Management Password
ACTUATOR_USERadminUsername to access the Actuator through Web
ACTUATOR_PASSWORDadminPassword to access the Actuator through Web

It is also possible, though hardly ever necessary, to configure the following aspects of the RabbitMQ setup:

Environment Variable NameDefault ValueExplanation
RABBITMQ_EXCHANGEantragsgruen-exchangeThe exchange that Antragsgrün is supposed to publish to
RABBITMQ_EXCHANGE_DEADantragsgruen-exchange-deadThe exchange that failed messages are published to
RABBITMQ_QUEUE_USERantragsgruen-queue-userThe queue for user-targeted messages
RABBITMQ_QUEUE_USER_DEADantragsgruen-queue-user-deadThe dead letter queue for user-targeted messages
RABBITMQ_QUEUE_SPEECHantragsgruen-queue-speechThe queue for speaking-list related messages
RABBITMQ_QUEUE_SPEECH_DEADantragsgruen-queue-speech-deadThe dead letter queue for speaking-list related messages

Compiling for GraalVM

Setup on macOS:

brew install --cask graalvm/tap/graalvm-jdk21
export JAVA_HOME=/Library/Java/JavaVirtualMachines/graalvm-jdk-21/Contents/Home

Compiling and running:

./mvnw native:compile -Pnative
./target/live

Running with Docker (JRE)

A dummy docker-compose.yml is provided that builds and runs the application. To set it up:

docker compose -f docker-compose.jdk.yml build
docker compose -f docker-compose.jdk.yml up

This will expose services:

Testing

Running spotbugs && checkstyle

./mvnw compile && ./mvnw spotbugs:check && ./mvnw checkstyle:check

Running the integration tests

Currently, there is one integration test that tests the RabbitMQ-receiver, the data mapping and the WS/STOMP-Server by connecting to the STOMP-Server using a self-signed JWT for authentication, then sends a message to RabbitMQ and tests what message gets delivered through the STOMP-Connection.

The test case is located in LiveApplicationTests.java, some helper classes in utils and the test fixtures (JSON Payloads) in resources.

To run the tests, call:

npm ci # Only needed once
./mvnw test

Note that Docker needs to be installed for this, too, as the integration test makes use of RabbitMQ.

JWTs for tests

The public / private keys used for the test cases were created using the following commands:

ssh-keygen -t rsa -b 4096 -m PEM -f bundle.pem
openssl rsa -in bundle.pem -pubout -outform PEM -out jwt-test-public.key
openssl pkcs8 -topk8 -inform PEM -outform PEM -in bundle.pem -out jwt-test-private.key -nocrypt