Skip to main content

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.

  1. ApplicationsAsk the top broker
  2. answers, notifications
  3. Region brokerRegisters the cities, source alias region
  4. answers from the city
  5. City brokerRegisters the departments, source alias city
  6. answers from the department
  7. Parks brokerHolds the data, source alias parks
    Site gatewaySources of the parks
AndromedaA system Andromeda doesn't run

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.

  • Loops are broken by the Via header

    The header lists the alias of every broker the request has passed. A broker that finds its own alias there doesn't forward the request, and the answer carries a 199 warning. This only works when every broker at every level has its own federation.identity.source_alias, because two brokers on different levels with the same alias would block each other's requests.

  • Failures travel up as warnings

    A source that fails at a lower level becomes a warning on that level's answer, and the warning travels up with the answer. The levels above see a degraded answer from the city rather than an error.

  • Every level bounds its own work

    Each level limits what it gathers with federation.query.max_entities. A lower level refuses a query that is too large with 403 TooManyResults, and the level above sees that as a failed source.

What to set​

  • federation.forwarding.request_timeout

    Make it larger at the upper levels. The region broker waits for the city broker, which waits for its departments, so the region's timeout has to cover the whole chain below it. With 5 seconds at the region over 5 seconds at the city, the city looks failed whenever one department is slow. A registration's own management.timeout takes precedence, so you can also set it source by source.

  • request_limits.timeout

    The longest one request may run, 30 seconds by default. It bounds the whole chain, so the waits down the chain have to add up to less than it, and every level's federation.forwarding.request_timeout has to stay below it.

  • federation.cooldown.mode

    Set it at every level. A failing source is held back by the level that asks it, so a department that is down stops costing the city a wait on every request, and the region never notices.

  • federation.identity.source_alias and federation.identity.source_id

    Unique for every broker, across all the levels.

  • server.public_url

    The address the level above reaches this broker at, so that subscriptions can be forwarded down the chain.

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.

BrokerForwarding timeoutWaits for
andromeda, the region12 secondsThe city, and through it the parks broker
andromeda-city5 secondsThe parks broker
andromeda-parks5 seconds, the defaultNo 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
docker-compose.yaml
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'

If it goes wrong​

  • A deep level answers, and the level above reports it as failed

    The upper level's timeout is shorter than the time the lower level needs. Raise federation.forwarding.request_timeout or the registration's management.timeout at the upper level.

  • A request is not forwarded, and the answer carries a 199 warning

    A broker found its own alias in the Via header. Either two brokers share a federation.identity.source_alias, or two registrations lead back to each other. Give every broker its own alias, and remove the registration that closes the circle.

  • The top broker's answer is missing entities the city holds

    Check each registration in turn, starting from the top. A level forwards a query only when its registration matches the types, attributes, area and scope of the query, and one registration that is too narrow hides everything below it.