Bug reporter information (required)
Email or GitHub username GavinPizza
Bug description
In a multitenant database, OpenLogReplicator silently discards every transaction on a pluggable database whose DBID and CON_UID differ.
The PDB filter resolves the local container's identity from V$PDBS.DBID and compares it against the container identifier carried in the 5.2 begin-transaction redo vector. Those are two different Oracle identifiers. They hold the same value on a PDB created in place, so the comparison succeeds. They diverge on a plugged PDB, so every begin vector fails the comparison and is discarded, and with no begin record the commit vectors have nothing to attach to.
Nothing in the running process indicates a problem:
- the process stays alive and responsive
bytes_read, bytes_parsed, checkpoints and log_switches all advance normally
- checkpoint files are written on schedule with correct contents
- the writer stays connected
- no error, exception, non-zero exit or warning is logged
Warning 60011 ("transaction with no beginning") does not fire either, since the record it would fire on is the one being discarded.
A second PDB in the same container database, on the same redo stream and the same binary, replicates correctly throughout. The only difference between them is whether DBID and CON_UID are equal.
Step-by-step reproduction instructions (required)
This is the tutorials/Oracle-to-file tutorial, run exactly as its README documents, with one PDB added and one config field changed. Steps 1 to 3 are the unmodified tutorial and establish the control. Steps 4 onward are the delta.
-
Build the two images using the tutorial's own scripts. Both are public; neither requires a private registry.
git clone https://github.com/bersler/OpenLogReplicator-tutorials.git
cd OpenLogReplicator-tutorials/images
# Oracle 21c XE, via Oracle's own buildContainerImage.sh
./oracle_database_21.3.0-xe.sh
# OpenLogReplicator v2.0.0: must be pinned; the script defaults to 'master'
OPENLOGREPLICATOR_VERSION=2.0.0 ./bersler_openlogreplicator_tutorial_prod.sh
This produces oracle/database:21.3.0-xe and bersler/openlogreplicator:tutorial, the two image names cfg.sh expects.
bersler_openlogreplicator_tutorial_prod.sh refuses to run as root. Run it as a non-root user in the docker group, and make sure that user can read its Docker config directory. On Docker 29 build is the buildx plugin, so an unreadable config dir makes docker build fail with a misleading unknown shorthand flag: 't' in -t.
Everything from here runs in the tutorial directory:
cd ../tutorials/Oracle-to-file
-
Check the images, per the tutorial README:
-
Create and start the database:
This brings up compose.yaml, which puts the oracle service on the internal network with oradata, fra, sql and setup bind-mounted. It then waits for DATABASE IS READY TO USE and runs sql/schema-usrtbl.sql in XEPDB1. Archivelog mode and database-level supplemental logging are applied automatically along the way: setup/config.sql is mounted at /opt/oracle/scripts/setup, which the Oracle image executes at database creation. No manual archivelog step is needed.
On a host with many CPU cores, edit the oracle service in compose.yaml before running this. Oracle derives the minimum SGA from cpu_count, and on a 48-core host the default XE sga_target of 1536M is rejected with ORA-00821: ... needs to be at least 2256M, which aborts database creation:
environment:
- ORACLE_CHARACTERSET=AL32UTF8
- INIT_SGA_SIZE=3072
cpuset: "0-7"
build:
context: .
shm_size: '4gb'
shm_size: '4gb'
This is the only change made to any tutorial file outside scripts/OpenLogReplicator.json. On a smaller host the shipped values work unaltered.
-
Run the tutorial to completion, unmodified. This is the control:
./3.olr-start.sh
./4.test.sh
4.test.sh waits for exactly 9 lines in output/results.txt, prints them, and exits 0. Replication works on XEPDB1, whose DBID and CON_UID are equal:
{"scn":2625756,"tm":1785440696000000000,"c_scn":2625756,"c_idx":1,"xid":"0x000a.005.0000028b","payload":[{"op":"begin"}]}
{"c_scn":2625756,"c_idx":2,"xid":"0x000a.005.0000028b","payload":[{"op":"c","schema":{"owner":"USRTBL","table":"ADAM1"},"after":{"ID":1,"NAME":"Adam Leszczyński","COUNT":10,"START_TIME":1564662896000000000}}]}
{"c_scn":2625756,"c_idx":3,"xid":"0x000a.005.0000028b","payload":[{"op":"commit"}]}
{"scn":2625765,"tm":1785440696000000000,"c_scn":2625765,"c_idx":1,"xid":"0x0005.01a.00000229","payload":[{"op":"begin"}]}
{"c_scn":2625765,"c_idx":2,"xid":"0x0005.01a.00000229","payload":[{"op":"u","schema":{"owner":"USRTBL","table":"ADAM1"},"before":{"ID":1,"COUNT":10},"after":{"ID":1,"COUNT":11}}]}
{"c_scn":2625765,"c_idx":3,"xid":"0x0005.01a.00000229","payload":[{"op":"commit"}]}
{"scn":2625768,"tm":1785440696000000000,"c_scn":2625768,"c_idx":1,"xid":"0x0004.011.00000252","payload":[{"op":"begin"}]}
{"c_scn":2625768,"c_idx":2,"xid":"0x0004.011.00000252","payload":[{"op":"d","schema":{"owner":"USRTBL","table":"ADAM1"},"before":{"ID":1,"NAME":"Adam Leszczyński","COUNT":11,"START_TIME":1564662896000000000}}]}
{"c_scn":2625768,"c_idx":3,"xid":"0x0004.011.00000252","payload":[{"op":"commit"}]}
Reset before the affected run:
This removes the OpenLogReplicator container, drops USROLR from XEPDB1, and deletes checkpoint, log and output. The database and USRTBL stay.
-
Add a second PDB whose DBID and CON_UID differ. XEPDB1 stays exactly as the tutorial left it and remains the control.
db_sql() in tutorials/common/functions.sh hardcodes ALTER SESSION SET CONTAINER = XEPDB1, so it cannot be used for this. Run against the container directly:
docker exec -i ORA1 /bin/bash -c 'export ORACLE_SID=XE ORAENV_ASK=NO
. oraenv
sqlplus -S / as sysdba' <<'EOF'
ALTER SESSION SET CONTAINER = CDB$ROOT;
CREATE PLUGGABLE DATABASE P1 ADMIN USER pdbadm IDENTIFIED BY pdbadmpwd;
ALTER PLUGGABLE DATABASE P1 OPEN;
-- unplug and plug the same descriptor back: this mints a fresh CON_UID
-- while DBID carries through from the descriptor
ALTER PLUGGABLE DATABASE P1 CLOSE IMMEDIATE;
ALTER PLUGGABLE DATABASE P1 UNPLUG INTO '/tmp/p1.xml';
DROP PLUGGABLE DATABASE P1 KEEP DATAFILES;
CREATE PLUGGABLE DATABASE P1 USING '/tmp/p1.xml' NOCOPY TEMPFILE REUSE;
ALTER PLUGGABLE DATABASE P1 OPEN;
SET LINESIZE 100 PAGESIZE 50
COLUMN NAME FORMAT A12
SELECT CON_ID, NAME, DBID, CON_UID FROM V$PDBS ORDER BY CON_ID;
SELECT NAME, DBID, CON_UID FROM V$PDBS WHERE DBID != CON_UID;
EOF
DB_CREATE_FILE_DEST is already set to /opt/oracle/oradata by setup/config.sql in step 2, so the datafiles land under the bind-mounted oradata directory that OpenLogReplicator reads through path-mapping.
The last two queries confirm the setup. The affected PDB must appear in the second one, the control must not (values differ per run; only the equal/divergent pattern matters):
CON_ID NAME DBID CON_UID
---------- ------------ ---------- ----------
2 PDB$SEED 2715856892 2715856892
3 XEPDB1 4170075421 4170075421
5 P1 1982200201 3620585990
NAME DBID CON_UID
------------ ---------- ----------
P1 1982200201 3620585990
XEPDB1 (control) and PDB$SEED have DBID equal to CON_UID. P1 (affected) does not, so it is the only row the second query returns.
-
Give P1 the same two users the tutorial created in XEPDB1. Only P1 needs this: XEPDB1 got USRTBL from 2.db-start.sh, and gets USROLR back from 3.olr-start.sh in step 8.
The table owner is the maintainer's own sql/schema-usrtbl.sql with only the container changed, and without its leading DROP TABLESPACE since P1 is new:
docker exec -i ORA1 /bin/bash -c 'export ORACLE_SID=XE ORAENV_ASK=NO
. oraenv
sqlplus -S / as sysdba' <<'EOF'
ALTER SESSION SET CONTAINER = P1;
CREATE TABLESPACE TBLS1 DATAFILE SIZE 100M AUTOEXTEND ON NEXT 100M;
ALTER TABLESPACE TBLS1 FORCE LOGGING;
CREATE USER USRTBL IDENTIFIED BY USRTBLPWD
DEFAULT TABLESPACE TBLS1 TEMPORARY TABLESPACE TEMP;
ALTER USER USRTBL QUOTA UNLIMITED ON TBLS1;
GRANT CONNECT TO USRTBL;
GRANT RESOURCE TO USRTBL;
ALTER SESSION SET CONTAINER = CDB$ROOT;
ALTER SYSTEM ARCHIVE LOG CURRENT;
EOF
Then apply sql/schema-usrolr.sql unmodified in P1. It already grants SELECT ON SYS.V_$PDBS, which this bug depends on. The file carries no ALTER SESSION of its own, so the container set below is what decides where it lands:
docker exec -i ORA1 /bin/bash -c 'export ORACLE_SID=XE ORAENV_ASK=NO
. oraenv
sqlplus -S / as sysdba' <<'EOF'
ALTER SESSION SET CONTAINER = P1;
@/opt/sql/schema-usrolr.sql
EOF
On a first run this reports ORA-01918: user 'USROLR' does not exist, because schema-usrolr.sql opens with DROP USER USROLR CASCADE; and expects to be re-run. It is harmless: SQL*Plus continues, and the user is created by the next statement.
Verified: 47 object grants land on USROLR, SELECT ON SYS.V_$PDBS among them.
-
Create the test table in each PDB and enable supplemental logging on it. XEPDB1 needs it again because test.sql ends with DROP TABLE, so step 3 left no table behind. This is the table definition from sql/test.sql, unmodified:
ALTER SESSION SET CONTAINER = <P1 | XEPDB1>;
CREATE TABLE USRTBL.ADAM1(
ID NUMBER NOT NULL,
NAME VARCHAR2(30),
COUNT NUMBER,
START_TIME TIMESTAMP
);
ALTER TABLE USRTBL.ADAM1 ADD CONSTRAINT ADAM1PK PRIMARY KEY(ID);
ALTER TABLE USRTBL.ADAM1 ADD SUPPLEMENTAL LOG DATA (PRIMARY KEY) COLUMNS;
-
Point OpenLogReplicator at the affected PDB. This is scripts/OpenLogReplicator.json exactly as the tutorial ships it, the same file that produced the control output in step 3, with a single field changed: server, from //oracle:1521/XEPDB1 to //oracle:1521/P1. Nothing else differs, so the complete file is:
{
"version": "2.0.0",
"log-level": 3,
"metrics": {
"type": "prometheus",
"bind": "0.0.0.0:8080"
},
"source": [
{
"alias": "S1",
"name": "ORA1",
"reader": {
"type": "online",
"path-mapping": ["/opt/oracle/oradata", "/opt/oradata", "/opt/oracle/fra", "/opt/fra"],
"user": "USROLR",
"password": "USROLRPWD",
"server": "//oracle:1521/P1"
},
"format": {
"type": "json"
},
"filter": {
"table": [
{"owner": "USRTBL", "table": "ADAM.*"}
]
}
}
],
"target": [
{
"alias": "T1",
"source": "S1",
"writer": {"type": "file", "output": "/opt/output/results.txt"}
}
]
}
path-mapping is required whenever OLR runs in a different container from Oracle; without it the redo reader fails in a way unrelated to this bug. metrics is top level in 2.0.0, and the version string is validated.
This is the complete file used in the reproduction. Reverting that one field to the shipped value gives the control run in step 11.
-
Start OpenLogReplicator with the tutorial's own script, which now picks up the edited config:
It recreates checkpoint, log and output, brings up the openlogreplicator profile, and polls the Prometheus endpoint until service_state{state="replicating"} 1. It reaches that state on P1 and exits 0, which is part of the problem being reported: by every signal the tutorial checks, replication is running.
On this second invocation it also prints mkdir: cannot create directory 'dump': File exists, because 5.olr-drop.sh leaves dump in place. The script expects this and continues.
It also re-runs schema-usrolr.sql against XEPDB1, restoring the user that 5.olr-drop.sh dropped in step 3. That is harmless here and is what leaves XEPDB1 ready for the control run in step 11.
-
Execute the workload in the affected PDB. This is the DML from the same tutorials/Oracle-to-file/sql/test.sql, stopping after the second COMMIT:
ALTER SESSION SET CONTAINER = P1;
INSERT INTO USRTBL.ADAM1 VALUES (1, 'Adam Leszczynski', 10, TO_DATE('2019-08-01 12:34:56', 'YYYY-MM-DD HH24:MI:SS'));
COMMIT;
UPDATE USRTBL.ADAM1 SET COUNT = COUNT + 1;
COMMIT;
The one change from test.sql is spelling the name in ASCII. The original 'Adam Leszczyński' needs NLS_LANG set correctly on whichever client runs it, or the ń is stored as replacement characters. That is a client charset issue unrelated to this bug, and it only makes the output harder to compare.
-
Observe the incorrect behaviour.
Actual result: /opt/output/results.txt is never created. The output directory stays empty.
The DML did commit. Querying the affected PDB afterwards shows both statements landed:
ALTER SESSION SET CONTAINER = P1;
SELECT ID, COUNT FROM USRTBL.ADAM1;
ID COUNT
---------- ----------
1 11 -- inserted as 10, updated to 11
The complete OLR log for that run is 45 lines, reproduced in full below apart from the 20 - found: <table> schema-load lines elided in the middle. It contains one WARN, the cold-start notice that no checkpoint file exists yet, and no ERROR:
2026-07-30 19:47:55 INFO 00000 OpenLogReplicator v2.0.0 (C) 2018-2026 by Adam Leszczynski (aleszczynski@bersler.com), see LICENSE file for licensing information
2026-07-30 19:47:55 INFO 00000 arch: x86_64, build-arch: , system: Linux, release: 6.1.0-44-amd64, build: Release, compiled: 2026-07-28 23:10, modules: Kafka OCI Prometheus Protobuf
2026-07-30 19:47:55 INFO 00000 starting Prometheus metrics, listening on: 0.0.0.0:8080
2026-07-30 19:47:55 INFO 00000 adding source: S1
2026-07-30 19:47:55 INFO 00000 adding target: T1
2026-07-30 19:47:55 INFO 00000 writer is starting with Writer: file:/opt/output/results.txt
2026-07-30 19:47:55 WARN 10003 file: checkpoint/ORA1-chkpt.json - get metadata returned: No such file or directory
2026-07-30 19:47:55 INFO 00000 connecting to the database instance of ORA1 to //oracle:1521/P1
2026-07-30 19:47:55 INFO 00000 version: Oracle Database 21c Express Edition Release 21.0.0.0.0 - Production, context: XE, resetlogs: 0, activation: 0, con_id: 5, con_name: P1
2026-07-30 19:47:55 INFO 00000 loading character mapping for AL32UTF8
2026-07-30 19:47:55 INFO 00000 loading character mapping for AL16UTF16
2026-07-30 19:47:55 INFO 00000 online redo log: /opt/oradata/XE/redo01.log
2026-07-30 19:47:55 INFO 00000 online redo log: /opt/oradata/XE/redo02.log
2026-07-30 19:47:55 INFO 00000 online redo log: /opt/oradata/XE/redo03.log
2026-07-30 19:47:55 INFO 00000 timezone: +00:00, db-timezone: +00:00, log-timezone: +00:00, host-timezone: +00:00
2026-07-30 19:47:55 INFO 00000 Replicator for ORA1 in online mode is starting from NOW
2026-07-30 19:47:55 INFO 00000 current resetlogs is: 1239997100
2026-07-30 19:47:55 INFO 00000 first data SCN: 0
2026-07-30 19:47:55 INFO 00000 starting sequence not found - starting with new batch with seq: 12
2026-07-30 19:47:55 INFO 00000 reading dictionaries for scn: 2636366
...
2026-07-30 19:47:57 INFO 00000 - found: USRTBL.ADAM1 (dataobj: 75808, obj: 75808, columns: 4, lobs: 0, lob-idx: 0)
...
2026-07-30 19:47:57 INFO 00000 last confirmed scn: 2636366, starting sequence: 12, offset: 0
2026-07-30 19:47:57 INFO 00000 resume writer
2026-07-30 19:47:57 INFO 00000 found redo log version: 21.0.0, activation: 3121120875, resetlogs: 1239997100, page: 512, sequence: 12, SID: XE, endian: LITTLE
2026-07-30 19:47:57 INFO 00000 processing redo log: group: 3 scn: 2636347 to 0 seq: 12 path: /opt/oradata/XE/redo03.log offset: 1024
2026-07-30 19:47:57 INFO 00000 new activation detected: 3121120875
OLR found the filtered table USRTBL.ADAM1 during schema load, opened its writer on the output file, and reported itself replicating. Nothing further was logged: the workload committed at 19:49 and the log ends at 19:47:57. Warning 60011 never fires here, since the record it would fire on is the one being discarded.
The Prometheus endpoint the tutorial itself polls tells the same story. Scraped from the running container after the workload committed:
bytes_read 7438336
bytes_parsed 457216
bytes_confirmed 91312
checkpoints{filter="out"} 778
messages_confirmed 775
dml_ops{filter="out",type="insert"} 0
dml_ops{filter="out",type="update"} 0
dml_ops{filter="out",type="commit"} 0
messages_sent 0
service_state{state="replicating"} 1
The process reads and parses megabytes of redo, checkpoints 778 times, and reports replicating. Every dml_ops counter and messages_sent stay at zero.
Expected result: the six JSON records the control PDB produces in step 11.
-
Repeat steps 7 to 9 against the control PDB XEPDB1, to confirm the contrast holds for this exact workload and not only for the full test.sql of step 3. Restore server to the shipped //oracle:1521/XEPDB1, set CONTAINER = XEPDB1 in step 9, and change nothing else. Output is produced normally:
{"scn":2638500,"tm":1785441136000000000,"c_scn":2638500,"c_idx":1,"xid":"0x000a.003.00000291","payload":[{"op":"begin"}]}
{"c_scn":2638500,"c_idx":2,"xid":"0x000a.003.00000291","payload":[{"op":"c","schema":{"owner":"USRTBL","table":"ADAM1"},"after":{"ID":1,"NAME":"Adam Leszczynski","COUNT":10,"START_TIME":1564662896000000000}}]}
{"c_scn":2638500,"c_idx":3,"xid":"0x000a.003.00000291","payload":[{"op":"commit"}]}
{"scn":2638510,"tm":1785441136000000000,"c_scn":2638510,"c_idx":1,"xid":"0x000a.012.00000290","payload":[{"op":"begin"}]}
{"c_scn":2638510,"c_idx":2,"xid":"0x000a.012.00000290","payload":[{"op":"u","schema":{"owner":"USRTBL","table":"ADAM1"},"before":{"ID":1,"COUNT":10},"after":{"ID":1,"COUNT":11}}]}
{"c_scn":2638510,"c_idx":3,"xid":"0x000a.012.00000290","payload":[{"op":"commit"}]}
These six records appeared within one second of the COMMIT. On P1 the same workload produced nothing in 150 seconds. Run the control against the same binary in the same session, changing nothing but server.
Environment details (required)
-
Official binary signature/version:
2026-07-29 14:11:26 INFO 00000 OpenLogReplicator v2.0.0 (C) 2018-2026 by Adam Leszczynski (aleszczynski@bersler.com), see LICENSE file for licensing information
2026-07-29 14:11:26 INFO 00000 arch: x86_64, build-arch: , system: Linux, release: 6.1.0-44-amd64, build: Release, compiled: 2026-07-28 23:10, modules: Kafka OCI Prometheus Protobuf
Built from official v2.0.0 source via bersler/OpenLogReplicator-docker; no signed binary is published for any release.
-
OpenLogReplicator version or commit hash: v2.0.0 (release tag)
-
Oracle version and edition: Oracle Database 21c Express Edition, 21.3.0.0.0 (oracle/database:21.3.0-xe, built via Oracle's own buildContainerImage.sh -v 21.3.0 -x)
-
Operating system: Debian GNU/Linux, kernel 6.1.0-44-amd64
-
Docker version: 29.1.5
Technical environment (required)
- Oracle version and edition: Oracle Database 21c Express Edition, 21.3.0.0.0
- Operating system and kernel version: Linux 6.1.0-44-amd64
- RAC and/or ASM usage: No RAC. No ASM. Single instance, filesystem storage.
Multitenant configuration details
Single container database. The reproduction above uses two pluggable databases sharing one physical redo stream: XEPDB1 (control, DBID == CON_UID) and P1 (affected, DBID != CON_UID). No Data Guard.
We first hit this in production at larger scale: one CDB, 7 PDBs, one OpenLogReplicator process per PDB, each with its own config file, state directory and ports.
The split is visible per-PDB in V$PDBS: of the seven, the two that emitted nothing were exactly the two whose DBID and CON_UID had diverged. No other property tracked it.
Supporting evidence
Measurements from our own deployment, offered as observation rather than as a proposed change.
Divergence tracks the failure across seven PDBs
SELECT CON_ID, NAME, DBID, CON_UID FROM V$PDBS ORDER BY CON_ID;
| CON_ID |
NAME |
DBID |
CON_UID |
equal? |
CDC output |
| 2 |
PDB$SEED |
1678743204 |
1678743204 |
yes |
— |
| 3 |
PDB_A |
3442438105 |
3442438105 |
yes |
— |
| 4 |
PDB_B |
1060685388 |
1060685388 |
yes |
— |
| 6 |
DB1 |
3552666917 |
1243036531 |
no |
none |
| 7 |
DB2 |
1484546789 |
1529555758 |
no |
none |
| 9 |
TEST_1 |
1374007921 |
1374007921 |
yes |
correct |
| 10 |
TEST_2 |
3005762508 |
3005762508 |
yes |
— |
All seven share one redo stream and run the same binary. Both PDBs with divergent identifiers produced no output at all, while TEST_1, the matching-identifier PDB under the same CDC configuration, replicated correctly throughout.
Oracle's own container identity
Oracle diagnostic dump for the affected PDB:
PdbInfo: Id=6 Name=DB1 Uid=1243036531 DBid=3552666917
Guid=<redacted> State=OPEN
...
PostPlugCompletionSCN=0x00000010ededa6c4
con_id/con_uid/con_name: 6/1243036531/DB1
Oracle keys the container on con_id/con_uid/con_name. DBid is a separate attribute and is not part of that tuple. The non-zero PostPlugCompletionSCN indicates this PDB was plugged in rather than created in place.
Observed counters on an affected PDB
After a cold START at a current SCN with no prior checkpoint state:
bytes_read 283,785,728
bytes_parsed 238,671,872
checkpoints 12,452
dml_ops 0
messages_sent 0
A second affected PDB over the same window: bytes_read 277,113,344, bytes_parsed 234,126,336, checkpoints 12,201, dml_ops 0, messages_sent 0. A control PDB on the same redo stream and the same binary emitted normally throughout.
Committed DML on a filtered table with PRIMARY KEY LOGGING never appeared in the output and never incremented dml_ops. No warning was logged.
The two sides of the comparison are different identifiers
The local container identity is resolved by a query against V$PDBS, while the value it is compared against is read from the pdb field of the 5.2 begin-transaction vector. Those are not the same identifier.
Whatever the vector carries, on these PDBs it is not DBID. The comparison fails on exactly the PDBs where DBID and CON_UID diverge, and succeeds on every PDB where the two columns happen to coincide. That is what makes the defect invisible on a PDB created in place.
Oracle's own dump above is consistent with that reading: the container is keyed on con_id/con_uid/con_name, and DBid sits outside that tuple as a separate attribute.
Resolving the identity against the matching column instead, with no other change on the same host against the same redo, moved both silent PDBs to correct output:
| PDB |
bytes_parsed |
dml_ops |
messages_sent |
| DB1 |
7,709,696 |
647 |
30 |
| DB2 |
7,386,624 |
306 |
30 |
| TEST_1 |
33,571,840 |
1,425 |
33 |
Delivery was confirmed end to end by writing a row into Oracle and watching it arrive in the target database.
Reported as an observation about which identifier the redo carries, not as a proposed change. Verifying any change is for the maintainer and the private test suite.
Ruled out before we looked at the identifiers
- Schema load: 15,638 objects, 934 tables, 37,685 columns, matching the working PDB.
- Supplemental logging: 172 log groups, tables under test on
PRIMARY KEY LOGGING.
- Configuration: identical to the working PDB apart from name, ports, state path and
start-scn.
- Grants:
SELECT on SYS.V_$PDBS present, no 10034.
- Replay position: reproduced from a cold
START at a current SCN with all checkpoint files removed.
- Absence of workload: disproved by committed DML.
- Uninitialised memory: the resolved identity was stable and correct across six consecutive checkpoints in one run.
Acknowledgement
- Fully reproducible, step-by-step scenario provided
- Understood that non-reproducible bugs cannot be fixed
- Understood that incomplete reports may be closed without further action
Official Binary Signature/Version:
2026-07-29 14:11:26 INFO 00000 OpenLogReplicator v2.0.0 (C) 2018-2026 by Adam Leszczynski (aleszczynski@bersler.com), see LICENSE file for licensing information
2026-07-29 14:11:26 INFO 00000 arch: x86_64, build-arch: , system: Linux, release: 6.1.0-44-amd64, build: Release, compiled: 2026-07-28 23:10, modules: Kafka OCI Prometheus Protobuf
Built from the official v2.0.0 release source via bersler/OpenLogReplicator-docker. No signed binary is published for any release; every GitHub release ships zero binary assets, so this banner is the only version/build string the software emits.
Bug reporter information (required)
Email or GitHub username
GavinPizzaBug description
In a multitenant database, OpenLogReplicator silently discards every transaction on a pluggable database whose
DBIDandCON_UIDdiffer.The PDB filter resolves the local container's identity from
V$PDBS.DBIDand compares it against the container identifier carried in the 5.2 begin-transaction redo vector. Those are two different Oracle identifiers. They hold the same value on a PDB created in place, so the comparison succeeds. They diverge on a plugged PDB, so every begin vector fails the comparison and is discarded, and with no begin record the commit vectors have nothing to attach to.Nothing in the running process indicates a problem:
bytes_read,bytes_parsed,checkpointsandlog_switchesall advance normallyWarning
60011("transaction with no beginning") does not fire either, since the record it would fire on is the one being discarded.A second PDB in the same container database, on the same redo stream and the same binary, replicates correctly throughout. The only difference between them is whether
DBIDandCON_UIDare equal.Step-by-step reproduction instructions (required)
This is the
tutorials/Oracle-to-filetutorial, run exactly as its README documents, with one PDB added and one config field changed. Steps 1 to 3 are the unmodified tutorial and establish the control. Steps 4 onward are the delta.Build the two images using the tutorial's own scripts. Both are public; neither requires a private registry.
This produces
oracle/database:21.3.0-xeandbersler/openlogreplicator:tutorial, the two image namescfg.shexpects.bersler_openlogreplicator_tutorial_prod.shrefuses to run as root. Run it as a non-root user in thedockergroup, and make sure that user can read its Docker config directory. On Docker 29buildis the buildx plugin, so an unreadable config dir makesdocker buildfail with a misleadingunknown shorthand flag: 't' in -t.Everything from here runs in the tutorial directory:
cd ../tutorials/Oracle-to-fileCheck the images, per the tutorial README:
Create and start the database:
This brings up
compose.yaml, which puts theoracleservice on the internal network withoradata,fra,sqlandsetupbind-mounted. It then waits forDATABASE IS READY TO USEand runssql/schema-usrtbl.sqlinXEPDB1. Archivelog mode and database-level supplemental logging are applied automatically along the way:setup/config.sqlis mounted at/opt/oracle/scripts/setup, which the Oracle image executes at database creation. No manual archivelog step is needed.On a host with many CPU cores, edit the
oracleservice incompose.yamlbefore running this. Oracle derives the minimum SGA fromcpu_count, and on a 48-core host the default XEsga_targetof 1536M is rejected withORA-00821: ... needs to be at least 2256M, which aborts database creation:This is the only change made to any tutorial file outside
scripts/OpenLogReplicator.json. On a smaller host the shipped values work unaltered.Run the tutorial to completion, unmodified. This is the control:
4.test.shwaits for exactly 9 lines inoutput/results.txt, prints them, and exits 0. Replication works onXEPDB1, whoseDBIDandCON_UIDare equal:{"scn":2625756,"tm":1785440696000000000,"c_scn":2625756,"c_idx":1,"xid":"0x000a.005.0000028b","payload":[{"op":"begin"}]} {"c_scn":2625756,"c_idx":2,"xid":"0x000a.005.0000028b","payload":[{"op":"c","schema":{"owner":"USRTBL","table":"ADAM1"},"after":{"ID":1,"NAME":"Adam Leszczyński","COUNT":10,"START_TIME":1564662896000000000}}]} {"c_scn":2625756,"c_idx":3,"xid":"0x000a.005.0000028b","payload":[{"op":"commit"}]} {"scn":2625765,"tm":1785440696000000000,"c_scn":2625765,"c_idx":1,"xid":"0x0005.01a.00000229","payload":[{"op":"begin"}]} {"c_scn":2625765,"c_idx":2,"xid":"0x0005.01a.00000229","payload":[{"op":"u","schema":{"owner":"USRTBL","table":"ADAM1"},"before":{"ID":1,"COUNT":10},"after":{"ID":1,"COUNT":11}}]} {"c_scn":2625765,"c_idx":3,"xid":"0x0005.01a.00000229","payload":[{"op":"commit"}]} {"scn":2625768,"tm":1785440696000000000,"c_scn":2625768,"c_idx":1,"xid":"0x0004.011.00000252","payload":[{"op":"begin"}]} {"c_scn":2625768,"c_idx":2,"xid":"0x0004.011.00000252","payload":[{"op":"d","schema":{"owner":"USRTBL","table":"ADAM1"},"before":{"ID":1,"NAME":"Adam Leszczyński","COUNT":11,"START_TIME":1564662896000000000}}]} {"c_scn":2625768,"c_idx":3,"xid":"0x0004.011.00000252","payload":[{"op":"commit"}]}Reset before the affected run:
This removes the OpenLogReplicator container, drops
USROLRfromXEPDB1, and deletescheckpoint,logandoutput. The database andUSRTBLstay.Add a second PDB whose
DBIDandCON_UIDdiffer.XEPDB1stays exactly as the tutorial left it and remains the control.db_sql()intutorials/common/functions.shhardcodesALTER SESSION SET CONTAINER = XEPDB1, so it cannot be used for this. Run against the container directly:DB_CREATE_FILE_DESTis already set to/opt/oracle/oradatabysetup/config.sqlin step 2, so the datafiles land under the bind-mountedoradatadirectory that OpenLogReplicator reads throughpath-mapping.The last two queries confirm the setup. The affected PDB must appear in the second one, the control must not (values differ per run; only the equal/divergent pattern matters):
XEPDB1(control) andPDB$SEEDhaveDBIDequal toCON_UID.P1(affected) does not, so it is the only row the second query returns.Give P1 the same two users the tutorial created in
XEPDB1. Only P1 needs this:XEPDB1gotUSRTBLfrom2.db-start.sh, and getsUSROLRback from3.olr-start.shin step 8.The table owner is the maintainer's own
sql/schema-usrtbl.sqlwith only the container changed, and without its leadingDROP TABLESPACEsince P1 is new:Then apply
sql/schema-usrolr.sqlunmodified in P1. It already grantsSELECT ON SYS.V_$PDBS, which this bug depends on. The file carries noALTER SESSIONof its own, so the container set below is what decides where it lands:On a first run this reports
ORA-01918: user 'USROLR' does not exist, becauseschema-usrolr.sqlopens withDROP USER USROLR CASCADE;and expects to be re-run. It is harmless: SQL*Plus continues, and the user is created by the next statement.Verified: 47 object grants land on
USROLR,SELECT ON SYS.V_$PDBSamong them.Create the test table in each PDB and enable supplemental logging on it.
XEPDB1needs it again becausetest.sqlends withDROP TABLE, so step 3 left no table behind. This is the table definition fromsql/test.sql, unmodified:Point OpenLogReplicator at the affected PDB. This is
scripts/OpenLogReplicator.jsonexactly as the tutorial ships it, the same file that produced the control output in step 3, with a single field changed:server, from//oracle:1521/XEPDB1to//oracle:1521/P1. Nothing else differs, so the complete file is:{ "version": "2.0.0", "log-level": 3, "metrics": { "type": "prometheus", "bind": "0.0.0.0:8080" }, "source": [ { "alias": "S1", "name": "ORA1", "reader": { "type": "online", "path-mapping": ["/opt/oracle/oradata", "/opt/oradata", "/opt/oracle/fra", "/opt/fra"], "user": "USROLR", "password": "USROLRPWD", "server": "//oracle:1521/P1" }, "format": { "type": "json" }, "filter": { "table": [ {"owner": "USRTBL", "table": "ADAM.*"} ] } } ], "target": [ { "alias": "T1", "source": "S1", "writer": {"type": "file", "output": "/opt/output/results.txt"} } ] }path-mappingis required whenever OLR runs in a different container from Oracle; without it the redo reader fails in a way unrelated to this bug.metricsis top level in 2.0.0, and theversionstring is validated.This is the complete file used in the reproduction. Reverting that one field to the shipped value gives the control run in step 11.
Start OpenLogReplicator with the tutorial's own script, which now picks up the edited config:
It recreates
checkpoint,logandoutput, brings up theopenlogreplicatorprofile, and polls the Prometheus endpoint untilservice_state{state="replicating"} 1. It reaches that state on P1 and exits 0, which is part of the problem being reported: by every signal the tutorial checks, replication is running.On this second invocation it also prints
mkdir: cannot create directory 'dump': File exists, because5.olr-drop.shleavesdumpin place. The script expects this and continues.It also re-runs
schema-usrolr.sqlagainstXEPDB1, restoring the user that5.olr-drop.shdropped in step 3. That is harmless here and is what leavesXEPDB1ready for the control run in step 11.Execute the workload in the affected PDB. This is the DML from the same
tutorials/Oracle-to-file/sql/test.sql, stopping after the secondCOMMIT:The one change from
test.sqlis spelling the name in ASCII. The original'Adam Leszczyński'needsNLS_LANGset correctly on whichever client runs it, or theńis stored as replacement characters. That is a client charset issue unrelated to this bug, and it only makes the output harder to compare.Observe the incorrect behaviour.
Actual result:
/opt/output/results.txtis never created. The output directory stays empty.The DML did commit. Querying the affected PDB afterwards shows both statements landed:
The complete OLR log for that run is 45 lines, reproduced in full below apart from the 20
- found: <table>schema-load lines elided in the middle. It contains oneWARN, the cold-start notice that no checkpoint file exists yet, and noERROR:OLR found the filtered table
USRTBL.ADAM1during schema load, opened its writer on the output file, and reported itself replicating. Nothing further was logged: the workload committed at 19:49 and the log ends at 19:47:57. Warning60011never fires here, since the record it would fire on is the one being discarded.The Prometheus endpoint the tutorial itself polls tells the same story. Scraped from the running container after the workload committed:
The process reads and parses megabytes of redo, checkpoints 778 times, and reports
replicating. Everydml_opscounter andmessages_sentstay at zero.Expected result: the six JSON records the control PDB produces in step 11.
Repeat steps 7 to 9 against the control PDB
XEPDB1, to confirm the contrast holds for this exact workload and not only for the fulltest.sqlof step 3. Restoreserverto the shipped//oracle:1521/XEPDB1, setCONTAINER = XEPDB1in step 9, and change nothing else. Output is produced normally:{"scn":2638500,"tm":1785441136000000000,"c_scn":2638500,"c_idx":1,"xid":"0x000a.003.00000291","payload":[{"op":"begin"}]} {"c_scn":2638500,"c_idx":2,"xid":"0x000a.003.00000291","payload":[{"op":"c","schema":{"owner":"USRTBL","table":"ADAM1"},"after":{"ID":1,"NAME":"Adam Leszczynski","COUNT":10,"START_TIME":1564662896000000000}}]} {"c_scn":2638500,"c_idx":3,"xid":"0x000a.003.00000291","payload":[{"op":"commit"}]} {"scn":2638510,"tm":1785441136000000000,"c_scn":2638510,"c_idx":1,"xid":"0x000a.012.00000290","payload":[{"op":"begin"}]} {"c_scn":2638510,"c_idx":2,"xid":"0x000a.012.00000290","payload":[{"op":"u","schema":{"owner":"USRTBL","table":"ADAM1"},"before":{"ID":1,"COUNT":10},"after":{"ID":1,"COUNT":11}}]} {"c_scn":2638510,"c_idx":3,"xid":"0x000a.012.00000290","payload":[{"op":"commit"}]}These six records appeared within one second of the
COMMIT. On P1 the same workload produced nothing in 150 seconds. Run the control against the same binary in the same session, changing nothing butserver.Environment details (required)
Official binary signature/version:
Built from official v2.0.0 source via
bersler/OpenLogReplicator-docker; no signed binary is published for any release.OpenLogReplicator version or commit hash: v2.0.0 (release tag)
Oracle version and edition: Oracle Database 21c Express Edition, 21.3.0.0.0 (
oracle/database:21.3.0-xe, built via Oracle's ownbuildContainerImage.sh -v 21.3.0 -x)Operating system: Debian GNU/Linux, kernel 6.1.0-44-amd64
Docker version: 29.1.5
Technical environment (required)
Multitenant configuration details
Single container database. The reproduction above uses two pluggable databases sharing one physical redo stream:
XEPDB1(control,DBID == CON_UID) andP1(affected,DBID != CON_UID). No Data Guard.We first hit this in production at larger scale: one CDB, 7 PDBs, one OpenLogReplicator process per PDB, each with its own config file, state directory and ports.
The split is visible per-PDB in
V$PDBS: of the seven, the two that emitted nothing were exactly the two whoseDBIDandCON_UIDhad diverged. No other property tracked it.Supporting evidence
Measurements from our own deployment, offered as observation rather than as a proposed change.
Divergence tracks the failure across seven PDBs
All seven share one redo stream and run the same binary. Both PDBs with divergent identifiers produced no output at all, while
TEST_1, the matching-identifier PDB under the same CDC configuration, replicated correctly throughout.Oracle's own container identity
Oracle diagnostic dump for the affected PDB:
Oracle keys the container on
con_id/con_uid/con_name.DBidis a separate attribute and is not part of that tuple. The non-zeroPostPlugCompletionSCNindicates this PDB was plugged in rather than created in place.Observed counters on an affected PDB
After a cold
STARTat a current SCN with no prior checkpoint state:A second affected PDB over the same window:
bytes_read277,113,344,bytes_parsed234,126,336,checkpoints12,201,dml_ops0,messages_sent0. A control PDB on the same redo stream and the same binary emitted normally throughout.Committed DML on a filtered table with
PRIMARY KEY LOGGINGnever appeared in the output and never incrementeddml_ops. No warning was logged.The two sides of the comparison are different identifiers
The local container identity is resolved by a query against
V$PDBS, while the value it is compared against is read from the pdb field of the 5.2 begin-transaction vector. Those are not the same identifier.Whatever the vector carries, on these PDBs it is not
DBID. The comparison fails on exactly the PDBs whereDBIDandCON_UIDdiverge, and succeeds on every PDB where the two columns happen to coincide. That is what makes the defect invisible on a PDB created in place.Oracle's own dump above is consistent with that reading: the container is keyed on
con_id/con_uid/con_name, andDBidsits outside that tuple as a separate attribute.Resolving the identity against the matching column instead, with no other change on the same host against the same redo, moved both silent PDBs to correct output:
Delivery was confirmed end to end by writing a row into Oracle and watching it arrive in the target database.
Reported as an observation about which identifier the redo carries, not as a proposed change. Verifying any change is for the maintainer and the private test suite.
Ruled out before we looked at the identifiers
PRIMARY KEY LOGGING.start-scn.SELECTonSYS.V_$PDBSpresent, no 10034.STARTat a current SCN with all checkpoint files removed.Acknowledgement
Official Binary Signature/Version:
Built from the official v2.0.0 release source via
bersler/OpenLogReplicator-docker. No signed binary is published for any release; every GitHub release ships zero binary assets, so this banner is the only version/build string the software emits.