Skip to content

Repository files navigation

ParkFlow - Smart Distributed Parking System image

A municipal parking system built as a genuinely distributed application: three client apps over a 3-node MongoDB replica set and a 3-node RabbitMQ quorum-queue cluster, with a recommender tier that reaches agreement by majority-vote consensus. Deployed across a 12-machine cluster.

Developed over three rolling assignments, each building on the last, including red-team / blue-team security rounds against another team's system.

My contribution (full per-assignment breakdown in the tables below): the three client applications and CLI in phase 1; the RabbitMQ quorum-queue cluster and the Docker cluster deployment in phase 2; and the recommendation engine, its malicious-mode toggle and engine tests in phase 3.


Course: Distributed Systems - Semester 2, 5786 (2026) Institution: Kinneret College on the Sea of Galilee Assignment: #3 - Recommender and Consensus (builds on #1 and #2) Team size approved by lecturer: 3 (instead of 4-5)


Team

Assignment 3 Task Allocation

Name Assignment 3 Task Hours
Rotem Swisa Task 2 - Recommender Server logic & Malicious Mode (RecommendationEngine, RecommenderNode, RecommenderServer, engine tests) ~30
Lidor Ben Simon Task 3 - Consensus Protocol Engine (ConsensusVoter, ConsensusCoordinator, ClusterConfig) + Task 4 - DevOps, JUnit suite & 12-machine deployment docs ~30
Libar Vizman Task 1 - UI/CLI recommender changes (Customer GUI view + CLI recommend) + Task 5 - Blue Teaming code fixes & docs/Defense.md ~30

New components delivered in Assignment 3: a 3-node Recommender Server cluster (SUC-8) with a majority-vote consensus protocol and a per-node malicious mode toggle (GUI + CLI), plus the Round-2 Blue-Team security hardening (secrets out of source, plaintext AMQP off, HMAC + replay + nonce on every node). See docs/ConsensusProtocol.md and docs/Defense.md.

Assignment 2 Task Allocation

Name Assignment 2 Task Hours
Rotem Swisa Task 2 (RabbitMQ 3-node Quorum Queue cluster) + Task 4 (DevOps: docker-compose-cluster.yml, Javadoc, Testing Plan) ~25
Lidor Ben Simon Task 1 (Cluster-aware UI APIs with node fallback) + Task 5 (Blue Team: code fixes) ~25
Libar Vizman Task 3 (3-node MongoDB replica set) + Task 5 (Blue Team: Defence.md report) ~25

Assignment 1 Task Allocation (reference)

Name Assignment 1 Task Hours
Rotem Swisa Task 1 - UI (Customer / PEO / MO GUI + CLI) ~35
Lidor Ben Simon Task 3 - Database & Storage (SQLite DAO) ~35
Libar Vizman Task 2 + Task 4 - Queue Server, DevOps, Build, Test ~35

Red Team Test Accounts

For the red-team exercise the team shipped pre-built JARs together with three seeded accounts, one per role — Customer, PEO and MO. Those build artefacts are not part of this repository.

A note on the seeded credentials

The demo users are seeded in plaintext in DatabaseManager and MongoDBManager so that a fresh local database is immediately usable for grading and for the red-team round. They unlock nothing outside a local test database.

In a real deployment this is exactly what you would not do: passwords would be salted and hashed, and the seed would move out of source into an environment file. The Assignment 3 blue-team round moved the infrastructure secrets out of source — the Mongo keyfile and the AMQP credentials now come from .env, and every node message carries an HMAC signature with replay and nonce protection. Application-level password hashing was outside the scope of the assignment.


What this project is

ParkFlow (brand name) is our implementation of the lecturer's "Mulligan" parking system. It has three client applications:

  • Customer app - start / stop parking, view history, live parking timer.
  • PEO app (Parking Enforcement Officer) - check vehicles, issue fines, review inspection history.
  • MO app (Municipality Officer) - reports of all parking payments and citations, including currently-open parkings.

Behind them are two server processes:

  • Database server - MongoDB 3-node replica set (rs0) shared across all clients over TCP. Falls back to a local SQLite file when MONGO_HOSTS is not set (local development mode).
  • Queue server - RabbitMQ 3-node cluster with two Quorum Queues: transactions and citations.

All code is Java 21, built with Gradle. See DEPLOY.md for both local and Dockerised run instructions.


Use cases covered

SUC Actor Description Where
SUC-1 Customer Start Parking Customer → DB
SUC-2 Customer Stop Parking → Transaction to queue Customer → DB + RabbitMQ
SUC-3 Customer Get parking-events list Customer → DB
SUC-4 PEO Check whether vehicle is legally parked PEO → DB (logs to SystemLog + PeoInspections)
SUC-5 PEO Issue citation PEO → DB + RabbitMQ
SUC-6 MO Transaction report MO → RabbitMQ queue (TCP/AMQP only, no DB)
SUC-7 MO Citation report MO → RabbitMQ queue (TCP/AMQP only, no DB)
SUC-8 Customer Parking Space Recommendation Customer → Recommender cluster → DB (majority-vote consensus)

Architecture - Assignment 3 (12-node cluster)

This is the physical architecture from Figure 1 of the assignment, with the new recommender server cluster added between the Customer app and the database.

┌──────────────┐        ┌──────────────┐        ┌─────────────────────┐
│ Customer UI  │        │   PEO UI     │        │       MO UI         │
│ (pub + SUC-8)│        │  (publisher) │        │ (subscriber ONLY)   │
└───┬─────┬────┘        └───────┬──────┘        └──────────┬──────────┘
    │     │ HMAC-signed TCP     │ MongoDB driver           │ TCP/AMQP
    │     │ (RECOMMENDER_HOSTS  │ (MONGO_HOSTS)            │ (RABBITMQ_HOSTS)
    │     ▼                     │                          │
    │  ┌───────────────────────────────────────────┐       │
    │  │      Recommender Server Cluster (SUC-8)   │       │
    │  │  recommender1 (LEADER) ─ recommender2 ─   │       │
    │  │  recommender3   | majority-vote consensus │       │
    │  │  + per-node Malicious Mode toggle         │       │
    │  └───────────────────────┬───────────────────┘       │
    │  MongoDB driver          │ reads zone snapshot       │
    ▼                          ▼  (busy + citation counts) │
┌──────────────────────────────────────┐                   │
│        MongoDB Replica Set  rs0      │    subscribe      │
│   mongo1 (primary)  mongo2  mongo3   │◄──────────────────┘
│   WriteConcern=MAJORITY              │
│   ReadPreference=primaryPreferred    │     ┌───────────────────────────┐
└──────────────────────────────────────┘     │  RabbitMQ 3-node Cluster  │
    ▲   ▲                                    │  (Quorum Queues)          │
    │   │  publish (RABBITMQ_HOSTS)          │  rabbitmq1 ─ rabbitmq2 ─  │
    └───┴────────────────────────────────────►  rabbitmq3                │
       (Customer + PEO publish transactions/ └───────────────────────────┘
        citations to the queue)

Flow for a recommendation (SUC-8): Customer → any recommender node (the entry node) → the leader collects each node's vote → majority vote → result back to the Customer. Each node reads the zone's spaces, their busy status, and their citation counts from MongoDB to compute its vote. See docs/ConsensusProtocol.md.

Key point: The MO app connects only to RabbitMQ via TCP/AMQP for its SUC-6/SUC-7 reports. (It also reads the DB for the appeals workflow and the "Citations / Space" report, which shows the counts the recommender uses.) Transaction and citation reports themselves come exclusively from the queue.

Failover behaviour

  • MongoDB: if the primary goes down, the replica set elects a new primary within ~10 s. The ReadPreference.primaryPreferred driver config falls back to a secondary for reads while the election is in progress.
  • RabbitMQ: RABBITMQ_HOSTS is a comma-separated list. The Java client iterates through it and connects to the first reachable node.
  • UI containers: MONGO_HOSTS and RABBITMQ_HOSTS env vars can be set to any subset of nodes; the clients retry automatically.

Important design note - RabbitMQ consumer mode. In our first iteration we registered consumers on every process that opened a RabbitMQ connection. That turned out to be a bug: when the PEO app published a citation, its own background consumer drained it back before the MO could see it. The fix is explicit subscriber mode: only the MO opens the connection with consume=true. Everything else is publisher-only.


How to run - the short version

First-time setup: this repo does NOT ship with the Gradle wrapper JAR (deliberately - it's a binary file). Open the project in IntelliJ once and it will generate gradle/wrapper/gradle-wrapper.jar, gradlew and gradlew.bat for you. After that, every command below works. Alternatively, run gradle wrapper --gradle-version 8.10 from the project root if you have a system Gradle installed.

Option A - Local development (single-node fallback)

No Docker required. Uses SQLite locally and a single RabbitMQ node.

# 1. make sure Java 21+ is on your PATH, and Docker Desktop is running

# 2. start RabbitMQ (single node - dev only)
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 \
    -e RABBITMQ_DEFAULT_USER=parkflow \
    -e RABBITMQ_DEFAULT_PASS="Mull1g@nSecure2026!" \
    rabbitmq:management

# 3. build
./gradlew build -x test

# 4. run GUI (open three separate terminals)
./gradlew :customer-ui:run
./gradlew :peo-ui:run
./gradlew :mo-ui:run

# 4. run CLI (open three separate terminals)
./gradlew :customer-ui:runCli
./gradlew :peo-ui:runCli
./gradlew :mo-ui:runCli

Status pill in the Customer/PEO/MO window shows Local DB in this mode.

Option A3 - Assignment 3: Recommender cluster on a single machine

This runs the full Stage-3 feature (SUC-8 + consensus) on one machine: a shared MongoDB, a RabbitMQ node, three recommender servers, and the Customer/PEO/MO clients. The three recommenders must share one database, so we use MongoDB here (not the per-process SQLite fallback).

# 1. infrastructure (shared DB + queue). No auth locally - simplest.
docker run -d --name parkflow-mongo  -p 27017:27017 mongo:7
docker run -d --name parkflow-rabbit -p 5672:5672 -p 15672:15672 `
  -e RABBITMQ_DEFAULT_USER=parkflow -e "RABBITMQ_DEFAULT_PASS=Mull1g@nSecure2026!" rabbitmq:3.13-management

# 2. build (also produces the recommender fat JAR)
.\gradlew build -x test
.\gradlew :recommender-server:shadowJar -x test

# 3. three recommender nodes - each in its OWN terminal (6001=leader, 6002, 6003).
#    Launch with -cp + the CLI class (NOT -jar: the jar's manifest points at the
#    JavaFX GUI class which is intentionally not bundled).
java "-DNODE_ID=recommender1" "-DRECOMMENDER_PORT=6001" `
  "-DRECOMMENDER_PEERS=recommender1=localhost:6001,recommender2=localhost:6002,recommender3=localhost:6003" `
  "-DRECOMMENDER_LEADER=recommender1" "-DMONGO_HOSTS=localhost:27017" `
  "-DHMAC_SECRET_KEY=PF-Dev-Local-2026!devOnly" `
  -cp recommender-server\build\libs\recommender-server-all.jar com.mulligan.recommender.cli.RecommenderCLI
#  ... repeat for recommender2/6002 and recommender3/6003 (change NODE_ID + RECOMMENDER_PORT)

# 4. clients (each in its own terminal), pointed at MongoDB + the recommender cluster
.\gradlew :customer-ui:run "-PMONGO_HOSTS=localhost:27017" "-PRECOMMENDER_HOSTS=localhost:6001,localhost:6002,localhost:6003"
.\gradlew :peo-ui:run "-PMONGO_HOSTS=localhost:27017"
.\gradlew :mo-ui:run  "-PMONGO_HOSTS=localhost:27017"

In the Customer app open Recommend Parking, enter a zone and a space, and submit. The leader's terminal prints the live vote tally and the majority decision. Flip a node to malicious mode by typing malicious on in its terminal (or use the GUI: :recommender-server:run). The MO app's Citations / Space page shows the citation counts the recommender uses.

MongoDB Compass (local, no auth): connect with mongodb://localhost:27017/?directConnection=true (no username/password - the local container runs without auth, unlike the Docker cluster below).

The complete, step-by-step single-machine guide with manual test scenarios and a demo script lives in docs/DEMO-PRESENTATION.md.

Option B - Full cluster (12 nodes)

Requires Docker Desktop. Starts the full 12-node cluster (3× MongoDB + 3× RabbitMQ + 3× Recommender + 3× UI).

Assignment 3 secrets note: the cluster no longer ships passwords in the compose file. Run cp .env.example .env first and put your secrets there (rotated values). In the host-run examples below, replace the legacy PFApp2026!mN8x / PFAdmin2026!K9x literals with the MONGO_APP_PASS / MONGO_ADMIN_PASS values from your .env.

# 1. build JARs first (Docker does not run Gradle inside the containers)
./gradlew build -x test

# 2. start the cluster (first run - builds Docker images)
docker compose -f docker-compose-cluster.yml up --build -d

# 2b. subsequent starts (images already built)
docker compose -f docker-compose-cluster.yml up -d

# 3. attach to the CLI sessions inside the containers (in separate terminals)
docker attach parkflow-customer
docker attach parkflow-peo
docker attach parkflow-mo

# To stop and wipe all data volumes (required if you change MongoDB auth):
docker compose -f docker-compose-cluster.yml down -v

Or run the GUI / CLI apps on the host with full replica-set failover:

One-time setup - add to C:\Windows\System32\drivers\etc\hosts (as Admin):

127.0.0.1  mongo1
127.0.0.2  mongo2
127.0.0.3  mongo3

Each loopback address maps to a different Docker port binding, so the MongoDB driver can resolve all three replica-set members from Windows and automatically failover when any node (including the primary) goes down.

Windows (PowerShell) - GUI:

.\gradlew :customer-ui:run "-PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017" -PMONGO_USER=parkflow_app "-PMONGO_PASS=PFApp2026!mN8x"
.\gradlew :peo-ui:run      "-PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017" -PMONGO_USER=parkflow_app "-PMONGO_PASS=PFApp2026!mN8x"
.\gradlew :mo-ui:run       "-PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017" -PMONGO_USER=parkflow_app "-PMONGO_PASS=PFApp2026!mN8x"

Windows (PowerShell) - CLI:

.\gradlew :customer-ui:runCli "-PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017" -PMONGO_USER=parkflow_app "-PMONGO_PASS=PFApp2026!mN8x"
.\gradlew :peo-ui:runCli      "-PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017" -PMONGO_USER=parkflow_app "-PMONGO_PASS=PFApp2026!mN8x"
.\gradlew :mo-ui:runCli       "-PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017" -PMONGO_USER=parkflow_app "-PMONGO_PASS=PFApp2026!mN8x"

When the cluster is active you should see [DB] Using MongoDB cluster: mongo1:27017,... on startup instead of [DB] Using local SQLite database.

Mac/Linux - GUI:

./gradlew :customer-ui:run -PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017 -PMONGO_USER=parkflow_app -PMONGO_PASS='PFApp2026!mN8x'
./gradlew :peo-ui:run      -PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017 -PMONGO_USER=parkflow_app -PMONGO_PASS='PFApp2026!mN8x'
./gradlew :mo-ui:run       -PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017 -PMONGO_USER=parkflow_app -PMONGO_PASS='PFApp2026!mN8x'

Mac/Linux - CLI:

./gradlew :customer-ui:runCli -PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017 -PMONGO_USER=parkflow_app -PMONGO_PASS='PFApp2026!mN8x'
./gradlew :peo-ui:runCli      -PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017 -PMONGO_USER=parkflow_app -PMONGO_PASS='PFApp2026!mN8x'
./gradlew :mo-ui:runCli       -PMONGO_HOSTS=mongo1:27017,mongo2:27017,mongo3:27017 -PMONGO_USER=parkflow_app -PMONGO_PASS='PFApp2026!mN8x'

Option B - Failover / resilience testing (GUI + CLI)

After adding the hosts file entries above, failover works from both GUI and CLI:

# Stop the primary - GUI and CLI keep working after ~10 s election
docker stop parkflow-mongo1

# Verify new primary was elected
# Note: use single quotes around the password in bash (! triggers history expansion in double quotes)
docker exec parkflow-mongo2 mongosh -u parkflow_admin -p 'PFAdmin2026!K9x' \
  --authenticationDatabase admin \
  --eval "rs.status().members.map(m => m.name + ' → ' + m.stateStr)" --quiet

# Bring it back
docker start parkflow-mongo1

Status pill shows MongoDB Cluster when MONGO_HOSTS is set.

Note - simultaneous failure of 2+ nodes: MongoDB uses Raft consensus. A 3-node replica set requires a majority (≥2) to elect a primary. Losing 2 nodes simultaneously leaves no quorum → no election → writes fail. This is expected and correct behaviour. The system is designed to survive one node failure at a time.

MongoDB Compass (GUI for the cluster)

Download from: https://www.mongodb.com/try/download/compass

Connect with this URI (requires the hosts file entries from above):

mongodb://parkflow_app:<MONGO_APP_PASS-from-.env>@mongo1:27017,mongo2:27017,mongo3:27017/parkflow?authSource=admin&replicaSet=rs0

See parkflow → Collections to browse live data during demo.

See DEPLOY.md for the full deployment instructions.


Documentation index

File What's in it
README.md This file - overview, team, quick-start.
DEPLOY.md Local and Docker deployment instructions.
docker-compose-cluster.yml Full 12-node cluster (3 Mongo + 3 RabbitMQ + 3 recommender + 3 UIs).
CLUSTER-DEPLOY-12MACHINES.md Standalone bare-metal deployment across 12 physical machines (Assignment 3).
docs/DEMO-PRESENTATION.md Single-machine run + manual test scenarios + demo script for the live presentation.
docs/ConsensusProtocol.md Recommender consensus protocol design (messages, steps, 8 scenarios).
docs/Defense.md Blue Team report: Round-2 root-cause + fixes (R2-C/I/CIA/A series).
.env.example Template for the gitignored .env holding all rotated secrets.
docs/DatabaseDesign.md SQLite schema, indexes, key queries.
docs/QueueServerDesign.md RabbitMQ queues, publisher vs subscriber mode, message format.
docs/TestingPlan.md Acceptance test tables - one per SUC.
docs/TestScenarios.md End-to-end walkthroughs for GUI and CLI.

Credits

Logos and illustrations produced by the team. RabbitMQ is used under the Mozilla Public License 2.0. SQLite is public domain. JavaFX is licensed under GPL v2 with Classpath Exception.

Some pictures from the project

צילום מסך 2026-08-16 190345 צילום מסך 2026-08-16 190524 צילום מסך 2026-08-16 190539 צילום מסך 2026-08-16 190627 צילום מסך 2026-08-16 190641 צילום מסך 2026-08-16 190655 צילום מסך 2026-08-16 191406 צילום מסך 2026-08-16 191438 צילום מסך 2026-08-16 191513 צילום מסך 2026-08-16 191532 צילום מסך 2026-08-16 191559 צילום מסך 2026-08-16 191847 צילום מסך 2026-08-16 192028 צילום מסך 2026-08-16 192058 צילום מסך 2026-08-16 191931

About

Distributed smart parking system - MongoDB replica set, RabbitMQ cluster, majority-vote consensus, 12-node deployment. Java 21

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages