# Sentinel lane. Three sentinels because a two-sentinel quorum cannot survive losing one, and a # failover test that cannot lose a sentinel is not testing failover. # # Host networking, not a bridge with published ports. Sentinel does not proxy: it answers # `SENTINEL get-master-addr-by-name` with the address it monitors, and the client then connects # there itself. On a bridge that address is the container's internal IP, which the client on the # host cannot reach, so the lane would resolve a primary it can never talk to — and after a # promotion it would resolve a different unreachable one. Sharing the host network namespace makes # the address Sentinel hands out the same address the client can dial, which is the only thing that # makes the promotion observable from outside. # # Ports are fixed rather than parameterised because Sentinel stores them in its own config: the # monitored address has to match what the client is told, and a lane whose two halves can disagree # is a lane that fails for reasons that are not the SDK's. # # primary 7010 · replica 7011 · sentinels 27010 27011 27012 # # The ACL file is loaded on both data nodes. The accounts are the deployment's last enforcement # boundary, so "they exist in standalone" is not evidence that they exist in the topology that will # actually be run in production. # # Both data nodes take their entire configuration from one definition, and that is load-bearing # rather than tidiness. These two nodes swap roles on every failover, so a setting written only into # the one that happens to start as primary silently stops applying the moment the lane does the # thing it exists to do. The lane learned this the hard way: min-replicas-to-write was set on the # primary only, the first promotion passed, and the second promotion — now writing to the node that # never had the setting — discarded 2,099 acknowledged writes. x-data-node: &data-node image: "redis:${REDIS_VERSION:-7.4}" network_mode: host volumes: - ../acl:/etc/redis/acl:ro entrypoint: - /bin/sh - -c # REPLICA_OF is deliberately unquoted: it is either empty or a two-word --replicaof argument. # # min-replicas-to-write is what stops a superseded primary from acknowledging writes it cannot # keep. Without it a promotion silently destroys them — measured here at eleven seconds and two # thousand confirmed-then-discarded writes — because Sentinel does not demote the old primary # until well after it has promoted the new one. Requiring an in-sync replica turns that window # into an explicit NOREPLICAS refusal the caller can see and act on. Any deployment where an # acknowledgement is supposed to mean something has to set these. - | exec redis-server \ --port $$NODE_PORT \ $$REPLICA_OF \ --appendonly no \ --save '' \ --min-replicas-to-write 1 \ --min-replicas-max-lag 1 \ --masteruser ca-skeleton-replication \ --masterauth fixture-replication \ --aclfile /etc/redis/acl/all-accounts.acl healthcheck: test: ["CMD-SHELL", "[ \"$$(redis-cli -p $$NODE_PORT --user ca-skeleton-application --pass fixture-application --no-auth-warning ping)\" = PONG ]"] interval: 2s timeout: 2s retries: 15 services: primary: <<: *data-node environment: NODE_PORT: "7010" REPLICA_OF: "" replica: <<: *data-node environment: NODE_PORT: "7011" REPLICA_OF: "--replicaof 127.0.0.1 7010" depends_on: primary: condition: service_healthy sentinel-1: &sentinel image: "redis:${REDIS_VERSION:-7.4}" network_mode: host # The config is written at start-up rather than mounted because Sentinel rewrites its own file # when it promotes. A read-only mount would make the first failover fail on a write error, and # a shared writable mount would have three sentinels rewriting one file. entrypoint: - /bin/sh - -c - | cat > /tmp/sentinel.conf <