This is the final project for the Distributed Systems UC for the second year of my Computer engeneering degree.
A distributed service for executing Python test suites across multiple worker nodes. Each node exposes a Flask API, joins a peer mesh, and can execute part of an evaluation locally or delegate work to other nodes. An Nginx container sits in front of the cluster and forwards client traffic to the bootstrap node.
The system accepts either:
- A
.zipfile containing one or more Python projects - A JSON payload with GitHub repository URLs to download and evaluate
Results are persisted per node and can be queried while an evaluation is running or after it completes.
The project is built from the following components:
src/run_node.py: container entrypoint; starts aMeshNodeprocess and the Flask API.src/node/meshNode.py: peer discovery, synchronization, health checks, evaluation lifecycle, and statistics.src/node/evaluator.py: extracts projects, detects tests, balances work across nodes, and aggregates results.src/testing/test_running.py: creates temporary virtual environments, installs project requirements, and runspytest.src/app/routes.py: HTTP API exposed by each node.docker-compose.yml: starts a 4-node cluster plus Nginx.default.conf: Nginx reverse proxy configuration.
Default cluster topology:
cnode1on port5001cnode2on port5002cnode3on port5003cnode4on port5004nginxon port80
- A node starts with its own address and, optionally, a bootstrap node.
- New nodes join the mesh through
/joinand receive the current peer list. - When an evaluation request arrives, the service:
- extracts or downloads the target projects
- detects Python projects by looking for
requirements.txtand tests - splits test files across the least-loaded nodes
- runs
pytestin isolated temporary virtual environments - merges partial results and persists the evaluation
- Nodes periodically ping peers and sync evaluations to tolerate failures.
- Docker
- Docker Compose
- Bash, for
boot.sh
If you want to run Python files directly outside Docker, install the dependencies in requirements.txt.
chmod +x boot.sh
./boot.shWhat the script does:
- stops and removes the current compose stack
- detects the host machine IP
- exports
HOST_REAL_IP - rebuilds and starts the cluster in detached mode
export HOST_REAL_IP=$(hostname -I | awk '{print $1}')
docker compose up -d --builddocker compose down --volumes --timeout 0For ad hoc testing, src/local_node.py can start standalone Docker containers without Compose.
Examples:
python src/local_node.py 5001
python src/local_node.py 5002 5001
python src/local_node.py 5003 192.168.1.20 5001Meaning:
python src/local_node.py 5001: start a new network on port5001python src/local_node.py 5002 5001: join the local bootstrap node on port5001python src/local_node.py 5003 192.168.1.20 5001: join a bootstrap node on another machine
When the full stack is running, the easiest entrypoint is http://localhost through Nginx.
Submit a new evaluation.
Supported content types:
multipart/form-datawith afilefield containing a zip archiveapplication/jsonwith repository URLs
Zip upload example:
curl -X POST http://localhost/evaluation \
-F "file=@projects.zip"GitHub repositories example:
curl -X POST http://localhost/evaluation \
-H "Content-Type: application/json" \
-d '{
"auth_token": "ghp_your_token",
"projects": [
"https://github.com/owner/project-a",
"https://github.com/owner/project-b"
]
}'Response:
{
"id": "evaluation-uuid"
}Returns all known evaluations aggregated from the cluster.
curl http://localhost/evaluationReturns the complete data for a specific evaluation.
curl http://localhost/evaluation/<eval_id>Typical fields include:
status_run_infomodule_results- per-project summaries
Returns node-level and global statistics collected from the cluster.
curl http://localhost/statsReturns the current peer view for the cluster.
curl http://localhost/networkReturns the peer list from the node that answered the request.
curl http://localhost/peersThese routes are used for peer-to-peer coordination and internal execution:
GET /evaluation_reqGET /evaluation_req/<eval_id>GET /stats_reqPOST /joinPOST /new_peerGET /pingPOST /run_testsGET /loadGET /kill
The exact response depends on the project under evaluation, but completed evaluations follow this general structure:
{
"status": "done",
"_run_info": {
"errors": 0,
"%passed": 87.5,
"%failed": 12.5,
"nota": 17.5,
"eval_time": 4.21
},
"module_results": {
"project-a": {
"tests/test_example.py": {
"%passed": 100.0,
"%failed": 0.0
}
}
}
}Important details:
- each test module is executed independently
- project requirements are installed before running tests
- temporary virtual environments are created under
/tmp - each node persists evaluations in files like
evaluations_<host>_<port>.json
.
├── src/
│ ├── app/ # Flask app and HTTP routes
│ ├── load_balance/ # Least-loaded distribution strategy
│ ├── node/ # Mesh node, evaluator, persistence
│ ├── testing/ # Pytest execution helpers and result aggregation
│ ├── local_node.py # Run standalone Docker nodes
│ └── run_node.py # Main runtime entrypoint
├── docs/ # Project report and specification PDFs
├── Dockerfile
├── docker-compose.yml
├── default.conf
├── boot.sh
└── requirements.txt
- Nginx currently forwards requests to
cnode1, which acts as the public entrypoint. - GitHub-based evaluation depends on GitHub API availability and a valid token when private repositories or rate limits are relevant.
- Test execution installs project dependencies dynamically, so failures in
pip installwill fail that project evaluation. - Persistence is file-based and local to each node container.
Additional project material is available in docs/Projecto CD 2025-3.pdf, docs/Relatorio.pdf, and docs/Porotocolo.pdf.