mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
14 hours ago 7925442d32b631630b78e214cb1d2a9d71bb920a
refs
author Valery Kharseko <vharseko@3a-systems.ru>
Tuesday, October 13, 2026 11:06 +0200
committer GitHub <noreply@github.com>
Tuesday, October 13, 2026 11:06 +0200
commit7925442d32b631630b78e214cb1d2a9d71bb920a
tree 645fea61579be766d7cf379fd71335f679360891 tree | zip | gz
parent 5b210f310f313b4170c6c9ab04e9642809d789b6 view | diff
[#1178] Give the background join of the Docker image a server role, keep a failed bootstrap or upgrade unhealthy, and test the upgrade from a released image (#1184)

## Summary

### #1178: a server role for the background join

`OPENDJ_REPLICATION_TYPE=simple` made every server a directory server
and a replication server at once. The one-shot `srs` and `sdsr` types,
the only way to run a replication server without data or a directory
server without one, are deprecated in its favour. `REPLICATION_ROLE` now
sets the part a server of `simple` takes: `combined` (the default, as
before), `directory` or `replication`.

- **Each side of `dsreplication enable` gets its own role.** Without the
role flags the tool gives both servers a replication server, so a join
through a directory server handed it one
(`ReplicationCliMain.configureServer`).
- The peer's role is read from the peer, under its join lock, in one
search of its `cn=config`. That search returns its replication server,
its domain and its backend for `BASE_DN`, plus the role it publishes
next to its state in `cn=Docker Join`.
- A peer that publishes no state is `combined`, as every server was
before roles. That covers a peer on an image before roles, or a master
that runs no join.
- **The role is recorded on the volume and does not change after that.**
`setup.sh` checks `REPLICATION_ROLE` before it sets anything up and
writes it to `.replication-role` in the data directory.
- A later start with another value only logs that the server keeps its
role. So does an unknown value.
- A volume set up by an image before roles is read once from its
replication configuration, then recorded. A replication server without a
domain counts as the replication server of `srs` only when
`REPLICATION_ROLE=replication` says so, or when it has no backend for
`BASE_DN` either, which only this image sets up, for a replication
server.
- **Membership, the reset of an unregistered server and the peers asked
about registration follow the role.**
- A replication server is a member through its replication server, and
holds no backend whose writes a reset would hold.
- A peer whose only replication configuration is a replication server is
asked about registration only when it publishes the role `replication`.
On a `combined` server that state is an enable that stopped half-way.
- **A replication server holds no data and is never a seed.**
- It goes the pending road, and nothing initializes it. On a later start
it publishes `rejoining` until a round finds it a member, so that no
peer enables through it before a reset takes it out.
- The first peer of `REPLICATION_PEERS` that holds data seeds the
topology. Replication servers listed ahead of it are passed over, by the
role they publish.
- **A directory server does not enable with a directory peer that no
replication server joined yet.** `dsreplication enable` refuses a
topology without one, and the log says why.
- **The repair of the replication server lists adds only peers that run
a replication server.**
- Each peer a list lacks is asked whether it runs one.
- A peer that does not answer is left for a later start, whatever the
roles: a directory server put into a list by mistake would stay there
for good, while a replication server left out is added later, and adds
this server to its own lists when it starts.
- **`setup.sh` creates no backend and loads no entries for a replication
server** of the background join.
- **`BASE_DN` goes into the search filters of the join escaped** (RFC
4515).

### #1182: a failed bootstrap stays unhealthy across restarts

`run.sh` marks a volume `.bootstrap-pending` before its bootstrap. It
removes the mark only once the whole bootstrap has succeeded: the
bootstrap script, and the `replicate.sh` of a one-shot type.

A start over a volume that still carries the mark starts the server so
it can be looked at. It runs the upgrade (start-ds refuses an instance
of another version) but not the join, never writes the health marker,
and logs what to do: remove the volume, or finish the setup by hand and
remove the mark. This holds for a volume of the background join as well:
an initialize from the topology would bring its data, but nothing else
the bootstrap was to configure. Once the mark is removed, such a volume
joins and initializes from the topology. Volumes of earlier images carry
no mark and start as before.

### #1185: a failed upgrade stays unhealthy across restarts

The upgrade records the new version in `buildinfo` before its
post-upgrade tasks (the index rebuilds and the verification of the DN
equality indexes), so after one of them failed, or was cut off by a
stop, the next run of `upgrade` found nothing to do and the container
reported itself healthy.

`run.sh` marks a volume `.upgrade-pending` before it upgrades it to
another version (major.minor.point of `data/config/buildinfo` against
the image's `template/config/buildinfo`, as `BuildVersion.equals`
compares them), and removes the mark once `upgrade` succeeded,
post-upgrade tasks included. A mark over a volume whose version is
already the image's can only come from an upgrade that recorded the
version and did not complete: that start stays unhealthy and logs what
to do (rebuild the indexes `upgrade.log` names and remove the mark, or
restore the backup). An upgrade that failed before it recorded the
version is run again, as before.

### #1183: the upgrade from a released image in CI

Both Docker jobs bootstrap a volume with
`openidentityplatform/opendj:${release_version}` (`-alpine` on the
Alpine job). They then start the build image over it and check three
things: the volume was upgraded (no "has already been upgraded"), the
container reports healthy, and it serves the entries.

It also covers #1185: a successful upgrade leaves no `.upgrade-pending`;
a mark over an upgraded volume keeps the container unhealthy until it is
removed; an upgrade failed by a read-only `buildinfo` (it fails in
`changeBuildInfoVersion`, after the upgrade tasks) leaves the mark. The
step warns and is skipped when the latest release has no image on Docker
Hub yet (the pull answers `manifest unknown`, or, on a daemon with the
containerd image store, `failed to resolve reference ...: not found`):
release.yml pushes the images after it publishes the release. Any other
failed pull fails the step.

A second new step covers #1182: a restart over a volume whose
`create-backend` failed stays unhealthy, and turns healthy once the
setup is finished by hand and the mark removed.

## Documentation

`README.md`:
- the new *Server roles* section;
- how `srs` and `sdsr` map onto `simple`;
- the Kubernetes layout with separate replication servers;
- `REPLICATION_ROLE` in the environment table;
- the `.bootstrap-pending` and `.upgrade-pending` marks.

Installation Guide (*To Run OpenDJ in Docker*, *To Upgrade a Docker
Container*, from #1179): a restart after a failed first start or a
failed upgrade stays unhealthy, and how to get past it by removing the
mark, instead of "remove the volume" and "do not restart".
Administration Guide, the replication NOTE: `REPLICATION_ROLE`, linked
to the standalone directory server and replication server sections.

`.github/scripts/docker-test-replication.sh` gains:
- the #534 topology (two directory and two replication servers): roles
held across restarts and when started again in another role, the roles
published and recorded, each replication server stopped in turn, with
the change made once both directory servers are connected to the other
one, and before the joins on empty volumes (#1188);
- the role of a volume set up before roles (no `.replication-role`)
inferred from its replication configuration, not from
`REPLICATION_ROLE`: combined, directory and replication volumes, the
directory and replication ones started under `combined`, a combined
server without its domain, and the same volume under
`REPLICATION_ROLE=replication`, as the replication server of `srs`
leaves it before roles;
- the repair of the replication server lists leaving a peer that does
not answer out, and adding it on a later start;
- `setup.sh` refusing an unknown role in a container before it sets
anything up;
- the time per case, longest first, at the end of every run, and every
wait that takes over 20 s, to find where the script spends its time;
- the failed background-join bootstrap (dj-bf) staying unhealthy until
the mark is removed, then initializing from the topology;
- a replication server and a directory server joining through a
replication server;
- directory servers with no replication server;
- a replication server listed first;
- an unknown role;
- a replica joining through a master that runs no join, which is given a
replication server.

## Testing

Run locally on a 1-CPU Docker VM (4 CPUs for the review round 3 checks),
with images built from this branch over a `5.2.0-SNAPSHOT` build of
master, and the timeouts of the script tripled:

- CI run 38311485177 (ee8b0681fe, review round 2) passed the whole
script in both Docker jobs.
- Review round 3, on dj-0/dj-1/dj-2 through the domain-less volume, the
repair case and the new `srs` volume case: passed on Debian (1221 s) and
Alpine (679 s). With `legacy_role` no longer reading
`REPLICATION_ROLE=replication`, the new case fails: `dj-1 was recorded
combined from a replication server and a backend under
REPLICATION_ROLE=replication`.
- The whole of `docker-test-replication.sh` on Debian passed, in 13401
s, before the fail-over step moved ahead of the joins on empty volumes
and the timings were added.
- On Alpine, the run before that move failed at the fail-over step,
which is how #1188 was found. Two runs after it failed earlier, at cases
this PR's last round does not change (the parallel start of three fresh
servers, the first start of the #534 topology): every join stopped
logging after its first rounds, while another test stand ran on the same
1-CPU VM and its disk was 98% full. The fail-over step in its new place
has not run locally on Alpine.
- Each case added in review round 2 fails against the change it pins:
`legacy_role` answering `$REPLICATION_ROLE`, its `server)` branch always
answering `replication`, the repair adding a peer that does not answer,
a `setup.sh` that refuses an unknown role only after it set the instance
up, and the `join.sh` of round 2 (the replication server without a
backend recorded `combined`).
- Round 1 (before review): the `REPLICATION_ROLE` block passed, and
failed on the scripts of `master` as expected (`the directory server
dj-d0 runs a replication server`).
- The CI steps *failed bootstrap* and *upgrade from a released image*
(with the #1185 checks) were run as GitHub runs a bash step (`bash -eo
pipefail`), with the same text as in the workflow, on Debian and Alpine:
they pass, the upgrade from `5.1.2` and `5.1.2-alpine`. *failed
bootstrap* on the `run.sh` of `master` fails: after the restart the
container reports `healthy` with no `userRoot`. The pull of the released
image, run the same way: a missing tag warns and skips, a missing
repository fails the step. The pattern of the skip was checked against
what a live pull of a missing tag answers on the classic image store
(Docker 29.7.2) and on the containerd one (`docker:29-dind`, 29.9.0),
and against sample messages of a rate limit, a refused repository and a
network failure, which still fail the step.
- The functions that tell a role from a replication configuration were
checked against 30 combinations of input.
- The changed AsciiDoc chapters render with AsciidoctorJ 2.5.3 (`-v`)
without warnings.

Not run locally: the rest of the workflow, which CI runs.

## Notes

- `dsreplication enable` between two replication servers finds `BASE_DN`
only on a directory server of the topology it can reach. A replication
server that joins through another one while every directory server is
down waits for one to come back. This is documented in the README.

Fixes #1178
Fixes #1182
Fixes #1183
Fixes #1185
11 files modified
1207 ■■■■■ changed files
.github/scripts/docker-test-replication.sh 381 ●●●●● diff | view | raw | blame | history
.github/workflows/build.yml 194 ●●●●● diff | view | raw | blame | history
opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-replication.adoc 2 ●●●●● diff | view | raw | blame | history
opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc 2 ●●●●● diff | view | raw | blame | history
opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-upgrade.adoc 2 ●●●●● diff | view | raw | blame | history
opendj-packages/opendj-docker/Dockerfile 2 ●●●●● diff | view | raw | blame | history
opendj-packages/opendj-docker/Dockerfile-alpine 2 ●●●●● diff | view | raw | blame | history
opendj-packages/opendj-docker/README.md 120 ●●●●● diff | view | raw | blame | history
opendj-packages/opendj-docker/bootstrap/join.sh 401 ●●●●● diff | view | raw | blame | history
opendj-packages/opendj-docker/bootstrap/setup.sh 38 ●●●●● diff | view | raw | blame | history
opendj-packages/opendj-docker/run.sh 63 ●●●●● diff | view | raw | blame | history