# A central group of two instances and two streamers, on one machine.
#
# The stand does not come up with a single `docker compose up`: the join window
# is opened by a live central, and the join token exists only after it starts.
# The order is on the documentation page next to this file.
#
# Required environment:
#   CATENA_VERSION  image tag
#   LICENSE_KEY     your license key

name: catena-redundancy

configs:
  central-a:
    content: |
      listeners:
        http:
          - port: 80
      central:
        admin_keys:
          - ${ADMIN_KEY:-lab-admin-key}
        # Roles are claims -- "this machine is ready to do this work" -- not
        # assignments: which instance actually does it is decided by
        # PostgreSQL, which is why both instances claim layouter and stats.
        # The migrate role is here only: one instance applies the schema.
        roles: [run, migrate, layouter, stats]
        # The address of THIS instance, for its neighbours in the group.
        # A load balancer address will not do: it would lead a neighbour
        # back to itself.
        advertise_url: http://central-a
      api_auth:
        login: ${CENTRAL_LOGIN:-admin}
        password: ${CENTRAL_PASSWORD:-pass}

  central-b:
    content: |
      listeners:
        http:
          - port: 80
      central:
        admin_keys:
          - ${ADMIN_KEY:-lab-admin-key}
        roles: [run, layouter, stats]
        advertise_url: http://central-b
      api_auth:
        login: ${CENTRAL_LOGIN:-admin}
        password: ${CENTRAL_PASSWORD:-pass}

  streamer-to-a:
    content: |
      listeners:
        http:
          - port: 80
      streams: []
      managed_by:
        # The operator label of the machine. It may be omitted -- central
        # issues an identity of its own -- but then the registry names this
        # machine by a number rather than by a name.
        name: streamer-1
        # The address at which central reaches this machine's management API.
        # Announced at registration, so central does not have to be told it by
        # hand afterwards. Without it the streamer works and broadcasts, but
        # central cannot collect its play sessions and says so.
        announce:
          api_address:
            host: streamer-1
            port: 80
        # The group has two addresses and the order is the order of
        # preference. The second address is mandatory rather than desirable:
        # a broken sync loop is the only sign of a streamer's liveness, and a
        # streamer left with a single dead address counts as offline a minute
        # and a half later -- after which the layouter moves its streams off a
        # live, broadcasting machine.
        url:
          - http://central-a
          - http://central-b
        join_token: ${JOIN_TOKEN:-}

  streamer-to-b:
    content: |
      listeners:
        http:
          - port: 80
      streams: []
      managed_by:
        name: streamer-2
        announce:
          api_address:
            host: streamer-2
            port: 80
        url:
          - http://central-b
          - http://central-a
        join_token: ${JOIN_TOKEN:-}

x-central: &central
  image: flussonic/catena:${CATENA_VERSION:?image tag is required}
  command: ["central", "run"]
  environment:
    CONFIG_PATH: /etc/sapsan/sapsan.yaml
    DATABASE_URL: postgres://central:central@postgres:5432/central
    # The control plane needs a license too: without one the process exits
    # at boot.
    SAPSAN_PRODUCT: catena
    LICENSE_KEY: ${LICENSE_KEY:?license key is required}
  depends_on:
    postgres:
      condition: service_healthy
  restart: unless-stopped

x-streamer: &streamer
  image: flussonic/sapsan:${CATENA_VERSION:?image tag is required}
  environment:
    CONFIG_PATH: /etc/sapsan/sapsan.yaml
    SAPSAN_PRODUCT: catena
    LICENSE_KEY: ${LICENSE_KEY:?license key is required}
  restart: unless-stopped

services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: central
      POSTGRES_USER: central
      POSTGRES_PASSWORD: central
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U central -d central"]
      interval: 2s
      timeout: 3s
      retries: 30
    restart: unless-stopped

  central-a:
    <<: *central
    hostname: central-a
    configs:
      - source: central-a
        target: /etc/sapsan/sapsan.yaml
    ports:
      - "8081:80"

  central-b:
    <<: *central
    hostname: central-b
    configs:
      - source: central-b
        target: /etc/sapsan/sapsan.yaml
    ports:
      - "8082:80"
    depends_on:
      postgres:
        condition: service_healthy
      # The schema is applied by central-a, so the second instance starts
      # after it.
      central-a:
        condition: service_started

  streamer-1:
    <<: *streamer
    hostname: streamer-1
    configs:
      - source: streamer-to-a
        target: /etc/sapsan/sapsan.yaml
    volumes:
      - state-1:/var/lib/sapsan

  streamer-2:
    <<: *streamer
    hostname: streamer-2
    configs:
      - source: streamer-to-b
        target: /etc/sapsan/sapsan.yaml
    volumes:
      - state-2:/var/lib/sapsan
    # Started after the first streamer rather than alongside it: a machine
    # generates its own identifier on first boot from the clock, so two of
    # them booting in the very same millisecond can arrive with the same one
    # and central then marks both as duplicates.
    depends_on:
      streamer-1:
        condition: service_started

volumes:
  pgdata:
  state-1:
  state-2:
