Hierarchical
Federations nest. A region federates its cities, a city federates its departments, and a department may be distributed over its sites. The specification allows for this explicitly, since a domain can itself be centralised, distributed or federated, and can even federate domains of its own. In Andromeda a registration at one level simply points at a broker that has registrations of its own, and nothing else changes.
- ApplicationsAsk the top broker
- answers, notifications
- Region brokerRegisters the cities, source alias region
- answers from the city
- City brokerRegisters the departments, source alias city
- answers from the department
- Parks brokerHolds the data, source alias parksSite gatewaySources of the parks
When it fits
- Organisations are nested, and each level has an operator who decides what it exposes.
- A level should answer for its own part without the levels above it. The city broker serves the city's applications whether or not the region broker is up.
- You want to be able to add a level without changing the ones below it.
Each level costs a hop. Nesting deeper than the organisations require only adds latency and more places to fail.
How a request travels
A query at the top broker goes to the registered brokers that match it. Each of them matches it against its own registrations, forwards it to whatever matches, merges the answers with its own data and returns one answer. Because every hop merges, the top broker sees the whole city as a single source.
What to set
The settings that are the same at every level are on the Federated page.
Example deployment
Three levels, each a broker with a database of its own, a region, a city and a parks department. Only the region publishes a port. The federation.forwarding.request_timeout values grow towards the top, so each level waits longer than the one below it needs.
| Broker | Forwarding timeout | Waits for |
|---|---|---|
andromeda, the region | 12 seconds | The city, and through it the parks broker |
andromeda-city | 5 seconds | The parks broker |
andromeda-parks | 5 seconds, the default | No sources |
Start it
cat > .env <<EOF
POSTGRES_PASSWORD_REGION=$(openssl rand -hex 24)
POSTGRES_PASSWORD_CITY=$(openssl rand -hex 24)
POSTGRES_PASSWORD_PARKS=$(openssl rand -hex 24)
EOF
name: andromeda
x-postgresql: &postgresql
image: ghcr.io/mskvarc/postgresql:18.6-timescaledb2.31.0-postgis3.6.4-tsl
restart: unless-stopped
mem_limit: 2g
cpus: 1
x-andromeda: &andromeda
image: ghcr.io/mskvarc/andromeda:1.0.0
restart: unless-stopped
services:
postgresql:
<<: *postgresql
environment:
POSTGRES_USER: andromeda
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD_REGION:?set it in .env}
POSTGRES_DB: andromeda
volumes:
- postgresql-data:/var/lib/postgresql
andromeda:
<<: *andromeda
depends_on:
postgresql:
condition: service_healthy
environment:
ANDROMEDA__DATABASE__CONNECTION: postgres://andromeda:${POSTGRES_PASSWORD_REGION}@postgresql:5432/andromeda
ANDROMEDA__SERVER__PUBLIC_URL: http://andromeda:1056/
ANDROMEDA__FEDERATION__IDENTITY__SOURCE_ALIAS: region
ANDROMEDA__FEDERATION__IDENTITY__SOURCE_ID: urn:ngsi-ld:ContextSourceIdentity:region
ANDROMEDA__FEDERATION__COOLDOWN__MODE: exponential
ANDROMEDA__FEDERATION__FORWARDING__REQUEST_TIMEOUT: 12s
ports:
- "1056:1056"
postgresql-city:
<<: *postgresql
environment:
POSTGRES_USER: andromeda
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD_CITY:?set it in .env}
POSTGRES_DB: andromeda
volumes:
- postgresql-city-data:/var/lib/postgresql
andromeda-city:
<<: *andromeda
depends_on:
postgresql-city:
condition: service_healthy
environment:
ANDROMEDA__DATABASE__CONNECTION: postgres://andromeda:${POSTGRES_PASSWORD_CITY}@postgresql-city:5432/andromeda
ANDROMEDA__SERVER__PUBLIC_URL: http://andromeda-city:1056/
ANDROMEDA__FEDERATION__IDENTITY__SOURCE_ALIAS: city
ANDROMEDA__FEDERATION__IDENTITY__SOURCE_ID: urn:ngsi-ld:ContextSourceIdentity:city
ANDROMEDA__FEDERATION__COOLDOWN__MODE: exponential
ANDROMEDA__FEDERATION__FORWARDING__REQUEST_TIMEOUT: 5s
postgresql-parks:
<<: *postgresql
environment:
POSTGRES_USER: andromeda
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD_PARKS:?set it in .env}
POSTGRES_DB: andromeda
volumes:
- postgresql-parks-data:/var/lib/postgresql
andromeda-parks:
<<: *andromeda
depends_on:
postgresql-parks:
condition: service_healthy
environment:
ANDROMEDA__DATABASE__CONNECTION: postgres://andromeda:${POSTGRES_PASSWORD_PARKS}@postgresql-parks:5432/andromeda
ANDROMEDA__SERVER__PUBLIC_URL: http://andromeda-parks:1056/
ANDROMEDA__FEDERATION__IDENTITY__SOURCE_ALIAS: parks
ANDROMEDA__FEDERATION__IDENTITY__SOURCE_ID: urn:ngsi-ld:ContextSourceIdentity:parks
volumes:
postgresql-data:
postgresql-city-data:
postgresql-parks-data:
Start it with docker compose up -d.
Register each level
Register each level at the broker above it. The city broker is a source of the region, and the parks broker is a source of the city. The city broker publishes no port, so the second call runs from a throwaway container on the Compose network, which name: andromeda calls andromeda_default.
curl -i http://localhost:1056/ngsi-ld/v1/csourceRegistrations \
-H 'Content-Type: application/json' \
-d '{
"id": "urn:ngsi-ld:ContextSourceRegistration:city",
"type": "ContextSourceRegistration",
"information": [
{
"entities": [
{
"type": "Park"
},
{
"type": "Tree"
},
{
"type": "Bus"
}
]
}
],
"endpoint": "http://andromeda-city:1056/"
}'
docker run --rm --network andromeda_default curlimages/curl \
-i http://andromeda-city:1056/ngsi-ld/v1/csourceRegistrations \
-H 'Content-Type: application/json' \
-d '{
"id": "urn:ngsi-ld:ContextSourceRegistration:parks",
"type": "ContextSourceRegistration",
"information": [
{
"entities": [
{
"type": "Park"
},
{
"type": "Tree"
}
]
}
],
"endpoint": "http://andromeda-parks:1056/"
}'
Query from the top
Write a Tree to the parks broker, at the bottom of the chain.
docker run --rm --network andromeda_default curlimages/curl \
-i http://andromeda-parks:1056/ngsi-ld/v1/entities \
-H 'Content-Type: application/json' \
-d '{
"id": "urn:ngsi-ld:Tree:1",
"type": "Tree"
}'
A query at the region now returns it, by way of the city.
curl -s 'http://localhost:1056/ngsi-ld/v1/entities?type=Tree' \
-H 'Accept: application/ld+json'