Oninit® Logwalker — Point-in-time recovery
ontape can stop a logical restore only at a whole-log boundary. Logwalker adds a point in time: the restore ends exactly at the last transaction committed at or before the time you choose, and everything committed after it is not restored. It works from the log backups you already have, needs no engine to make the cut, and has been tested end to end with real ontape restores on IDS 14.10.
How it works
- The cut looks like a salvaged log. A log is shortened so that it ends right after a COMMIT, like a partly-filled final log. Logical restore accepts that as the end of the logs and then runs its normal clean-up, which rolls back any transaction that was still open at the cut.
- The cut point is chosen from commit times. Logwalker scans the log in order and keeps everything through the last COMMIT whose time is at or before your point in time, stopping at the first later commit. It never binary-searches: a clock stepped backwards can leave timestamps out of order. Several commits in the same second are all kept.
- The clock is the engine's. The comparison uses the commit times the engine wrote into the log (the ones onlog -l and logwalker print), which can differ from a shell's date by about a second. Take the time from the log.
- The archive must come before the cut. The level-0 (and any level-1/2) archive must be older than the point in time. Logwalker does not check this for you.
- Backups are never modified. Cutting writes a new file from an offline copy; the restore-filter method (below) writes nothing at all.
Which approach?
| Your log backups are… | Use | What it does |
|---|---|---|
| plain files (LTAPEDEV a directory, no filter) | --truncate-at + --truncate-out [--keep-tail] worked example |
Writes a shortened copy of the log backup. You put it in place of the original and remove the later logs from the restore set. |
| written through a BACKUP_FILTER (for example gzip) | --restore-filter + --backup-filter + --truncate-at filtered backups |
Decodes the filtered file, cuts it, and re-encodes and re-frames it as a normal filtered backup. |
| written through a BACKUP_FILTER, and you want to choose the time at restore time | --pit-filter as the RESTORE_FILTER PIT restore filter |
The restore itself stops at the point in time; no backup file is created or changed and later logs need not be removed. |
The flags are described on the Operations page; this page shows how to use them.
Cut a log backup — a worked example
ontape can stop a logical restore only at a whole-log boundary. --truncate-at adds a point in time: it shortens one log backup so that the engine sees the cut as the real end of the logs. Everything committed at or before the time you give is restored; everything after it is not. No Informix instance is needed to make the cut — it works on a copy of the backup file — and the original is never modified.
The example below is a complete run, start to finish, on IDS 14.10.FC13W7 (LTAPEDEV a directory, LTAPEBLK 32, 2 KB pages). Nine rows are inserted into shop:orders, one committed every three seconds. The first three go in before the point in time, the next three after it in the same log, and the last three in the next log. Restoring to the point in time gives back exactly the first three.
1. Set up and take the archive
$ ontape -s -L 0 # level-0 archive, before the work
$ echo "create database shop with buffered log;
create table orders(id int, note varchar(40));" | dbaccess - -
(The archive here was taken just after the database was created; any level-0 archive taken before the point in time works.)
2. The work, and the point in time
Each insert is its own transaction. The point in time is picked between the third and fourth commit:
order 1 committed 2026-10-02 20:23:00
order 2 committed 2026-10-02 20:23:03
order 3 committed 2026-10-02 20:23:06
--- point in time: 2026-10-02 20:23:09
order 4 committed 2026-10-02 20:23:11
order 5 committed 2026-10-02 20:23:14
order 6 committed 2026-10-02 20:23:17
(log switch: onmode -l)
order 7 committed 2026-10-02 20:23:23
order 8 committed 2026-10-02 20:23:26
order 9 committed 2026-10-02 20:23:29
$ onmode -l ; ontape -a # back up every full log
/backup/ontape/logs now holds one file per log: docdemo_14_Log0000000009 contains orders 1–6 and docdemo_14_Log0000000010 contains orders 7–9.
3. Find the log that holds the point in time
Copy the log backups somewhere you can work on them, then look at the commit times in the log that should contain it. A backup file is not a raw page stream: it has a header block, then the log pages, then a trailer. For 2 KB pages the header block is LTAPEBLK KB, followed by one marker page, so the log pages start at (LTAPEBLK/2 + 1) pages × 2048 bytes: 17 pages = byte 34816 for the default LTAPEBLK 32, 9 pages = byte 18432 for LTAPEBLK 16 (both tested). They run for the log's used pages — the used column of onstat -l for that log (267 for log 9; a full log is LOGSIZE in KB divided by 2, e.g. 2500). With the default block size that gives --start 34816 --length 546816 (267 × 2048):
$ logwalker -n 9 --chunk docdemo_14_Log0000000009 \
--start 34816 --length 546816 -f COMMIT,BEGCOM | grep 'COMMIT at' | tail -5
LSN 9:0x0010a1c4 xid=30 COMMIT at 2026-10-02 20:23:03
LSN 9:0x0010a280 xid=30 COMMIT at 2026-10-02 20:23:06
LSN 9:0x0010a33c xid=30 COMMIT at 2026-10-02 20:23:11
LSN 9:0x0010a3f8 xid=30 COMMIT at 2026-10-02 20:23:14
LSN 9:0x0010a4b4 xid=30 COMMIT at 2026-10-02 20:23:17
(You can verify the layout: byte 32768 is the data-section marker and starts ff ff ff ff ff ff; byte 34816 is the first log page, with the log's uniqid at page offset 16.) A point in time of 20:23:09 falls between the commits at 20:23:06 and 20:23:11, so log 9 is the one to cut.
4. Cut
$ logwalker -n 9 --chunk docdemo_14_Log0000000009 \
--start 34816 --length 546816 \
--truncate-at "2026-10-02 20:23:09" \
--truncate-out cut/docdemo_14_Log0000000009 --keep-tail
# logwalker v0.12.0 — truncated uniqid 9 after the last COMMIT at or before the PIT
cut LSN 9:0x0010a2b8 (page 266 of the log, byte 696)
last commit 2026-10-02 20:23:06
commits kept 316 (of 317 seen before the first later commit)
output cut/docdemo_14_Log0000000009 (622592 bytes; 0 later page(s) dropped,
1348 gap byte(s) zeroed, backup trailer kept)
next install this file in place of log 9 and REMOVE every later log from the
restore set: xids are reused, so a later log's COMMIT can complete a
transaction left open at the cut
last commit is the order 3 commit. The records after it on that page (orders 4–6 and any work belonging to still-open transactions) were zeroed; here the cut fell on the log's last page, so no whole pages were dropped. A cut in the middle of a log drops the later pages and the output is shorter than the input by that many pages (the trailer still follows the cut because of --keep-tail).
Open transactions. If a transaction had begun in this log but not committed at the cut, the summary adds a line naming it, for example (from a different log, where one transaction was held open across the point in time):
open at cut 1 transaction(s) begun in this log, not yet committed: xid 30 (began 23:39:34)
Logical restore rolls such a transaction back — provided no later log is restored (see step 5). The count is a lower bound: a transaction that began in an earlier log is invisible to a one-file scan.
Which clock. The point in time is compared with the commit times the engine wrote into the log (the same times onlog -l and logwalker print), not with a shell's date. They can differ by about a second; in one of the tests the engine stamped a burst of commits the shell had timed at 23:39:36 with 23:39:35. Take the time from the log (step 3) rather than from a wall clock.
Exit status in this mode: 0 cut written, 1 error (including bad options), 2 the point in time is not inside this file — either every commit in it is at or before the time (the cut belongs to a later log, or this is the last log and should be restored whole) or none is (an earlier log). Exit 2 writes nothing, so a script can try each log in turn.
5. Restore with the cut log
Keep the originals, put the cut file in place of log 9, and take every later log out of the directory so the restore stops at the cut. This is not optional. Transaction ids are reused. In a test, log 10 was left in the restore set after a cut of log 9: the engine rolled it forward on top of the cut log, and a transaction that had been open at the cut (xid 30) came back as committed, because a transaction in log 10 reused xid 30 and its COMMIT completed the old one. Without log 10 the same restore removed it. Then restore as usual:
$ cp -a /backup/ontape/logs /backup/ontape/logs.orig # keep the originals
$ cp cut/docdemo_14_Log0000000009 /backup/ontape/logs/ # replace log 9
$ mkdir /backup/ontape/after-pit
$ mv /backup/ontape/logs/docdemo_14_Log0000000010 /backup/ontape/after-pit/
$ onmode -ky # engine offline
$ ontape -r # answers: y (continue), n (back up logs),
# n (level-1 archive), y (restore log tapes),
# then Return for each log file
...
Restore will use log backup file /backup/ontape/logs/docdemo_14_Log0000000007. Press Return to continue ...
Rollforward log file /backup/ontape/logs/docdemo_14_Log0000000007 ...
Rollforward log file /backup/ontape/logs/docdemo_14_Log0000000008 ...
Rollforward log file /backup/ontape/logs/docdemo_14_Log0000000009 ...
$ onmode -m # engine on-line
Before the restore the table held orders 1–9. After it:
$ echo "select id, note from orders order by id;" | dbaccess shop -
1 order 1
2 order 2
3 order 3
3 row(s) retrieved.
and online.log shows Logical Recovery Complete. Running the same restore with the original log 9 and no log 10 returns orders 1–6, so the cut is what removed 4–6.
6. Afterwards
- Take a level-0 archive immediately. The server now reuses log uniqids 10, 11, … with different contents from the old backups carrying the same numbers.
- Quarantine the old timeline's post-point-in-time logs (here after-pit/, and any later archives) so they can never be mixed into a restore of the new timeline.
Backups written with a BACKUP_FILTER
Informix can pipe archives and log backups through a filter command — BACKUP_FILTER /usr/bin/gzip and RESTORE_FILTER /usr/bin/gunzip in the onconfig. The file that ontape writes is then not a gzip file, which is why file reports just “data” and gunzip refuses it. This is the real layout, measured on a log backup from IDS 14.10.FC13W7 (LTAPEBLK 32, gzip):
block 0 @ 0x000000 plain tape header (not filtered)
page 0: ff ff ff ff ff ff 00 00 0c 00 09 00 ... "log backup tape IBM ..."
page 1: marker pages 2-15: zero padding
block 1 @ 0x008000 fc 7f 00 00 | 32764 bytes of gzip output 1f 8b 08 00 b9 5d c6 6a ...
block 2 @ 0x010000 fc 7f 00 00 | 32764 bytes, the same gzip stream continuing (no new header)
block 3 @ 0x018000 0f 4f 00 00 | 20239 bytes, ending in the gzip CRC32 + size | stale padding
block 4 @ 0x020000 ff ff ff ff ff ff 00 00 03 00 0a 00 ... end-of-tape page, plain
Each data block is a 4-byte little-endian length followed by that many bytes of the filter's output, one LTAPEBLK per block; the payloads joined together are one ordinary gzip member (85,767 bytes here). The end block is the normal end-of-tape page — its leading ff ff ff ff is the length word that tells a reader to stop. Decoded, the stream is exactly what an unfiltered backup holds after its header block: a marker page, the log pages (355 of them for this log 9), a trailer marker and the end-of-tape page.
Give logwalker the two filter commands from your onconfig and it handles all of that. Walking a filtered backup needs only the decoder:
$ logwalker -n 11 --chunk filthost_14_Log0000000011 --restore-filter gunzip -f COMMIT | grep 'COMMIT at'
Cutting needs the encoder as well. This is a real run (log 11, one data block, rows 11–13 committed before the point in time and rows 14–16 after it):
$ logwalker -n 11 --chunk filthost_14_Log0000000011 \
--restore-filter gunzip --backup-filter gzip \
--truncate-at "2026-10-07 15:01:22" \
--truncate-out cut/filthost_14_Log0000000011
# logwalker v0.12.0 — truncated uniqid 11 after the last COMMIT at or before the PIT
cut LSN 11:0x0002424c (page 36 of the log, byte 588)
last commit 2026-10-07 15:01:19
commits kept 39 (of 40 seen before the first later commit)
output <decoded stream> (131072 bytes; 0 later page(s) dropped, 1456 gap byte(s) zeroed, backup trailer kept)
next install this file in place of log 11 and REMOVE every later log from the restore set: ...
filtered cut/filthost_14_Log0000000011 (98304 bytes: header block + 1 framed data block(s) of
`gzip` output (8660 bytes) + end block)
The cut stream is run back through --backup-filter and re-framed; the original header block and end block are copied unchanged. Install it in place of log 11, remove log 12, and restore as in step 5 — the engine's own RESTORE_FILTER decodes it. After that restore the table held rows 1–9, 11, 12, 13 (rows 1–9 came from the archive; 11–13 exist only in the log, so the cut log was applied) and nothing committed after the point in time.
- Set sizes when you use a filter. With BACKUP_FILTER set, ontape refuses to run while TAPESIZE or LTAPESIZE is 0 (“The LTAPESIZE configuration parameter cannot be set to 0 when the BACKUP_FILTER configuration parameter is set”). TAPESIZE and LTAPESIZE can be changed with onmode -wf; BACKUP_FILTER and RESTORE_FILTER themselves must be set in the onconfig file and the engine restarted — onmode -wf does not accept them.
- Tested with gzip/gunzip on IDS 14.10.FC13W7 — walk, cut, re-frame, and a real restore. Another stdin→stdout filter should work the same way, because logwalker only runs the command and re-frames its output, but only gzip has been run against the engine. A filter that encrypts or adds its own header is handled the same way only if the matching decoder returns the plain backup stream.
- The block size is taken from your onconfig. logwalker reads LTAPEBLK from $ONCONFIG (printing the value and the file it came from), so run it with the same INFORMIXDIR/ONCONFIG as the engine. Elsewhere, or if the file can't be read, it warns and assumes 32 — use --tape-block to say otherwise. A value that doesn't match the file is rejected with a pointer to the setting rather than silently accepted.
- The decoded log is held in a private temporary directory (mode 0700, under $TMPDIR or /tmp) and removed when logwalker exits, including on Ctrl-C.
- Archives (level-0) written through a filter are not cut by this tool; only log backups are.
Stopping a restore at a point in time with a restore filter
Cutting backup files works, but there is a cleaner way when your backups are written through a BACKUP_FILTER: make the RESTORE_FILTER itself PIT-aware. Nothing on disk changes; you pick the point in time when you start the restore, and the same backups can be restored to a different time next week.
How the engine calls a restore filter (measured on IDS 14.10.FC13W7 with an instrumented filter): ontape itself runs it — once per tape, with no arguments, as the same operating-system user, with the environment inherited from ontape — and feeds the tape's joined payload stream on stdin (it starts 1f 8b 08 for gzip) and reads the decoded backup stream from stdout. The level-0 archive is presented twice (once for the archive information, once for the restore), then each log backup once. So a variable set when you start ontape -r reaches the filter, and the filter only has to tell an archive from a log by the first decoded page: a log backup's marker page has bytes 8–11 01 00 18 00, an archive's 00 00 14 00.
What logwalker --pit-filter does with each tape:
| Tape | Output |
|---|---|
| An archive (or anything that is not a log backup) | Passed through untouched, streaming — nothing is buffered, so a large archive is not held in memory. |
| A log that contains the point in time | Cut after the last COMMIT at or before it, exactly like --truncate-at; the rest of the log is not applied. |
| A log wholly before the point in time | Passed through whole. |
| A log wholly after the point in time | Emitted as a well-formed log with no log pages (the marker page and the trailer only), so the engine applies nothing from it. You do not need to remove later logs from the restore set. |
| No LW_PIT set | Everything passes through: an ordinary full restore. |
Setting it up
# /opt/logwalker/pit-restore.sh (executable; must run as the engine's OS user) #!/bin/bash exec /opt/logwalker/logwalker --pit-filter --restore-filter gunzip # onconfig: point the restore filter at it, then take the engine offline RESTORE_FILTER /opt/logwalker/pit-restore.sh $ onmode -ky $ LW_PIT="2026-10-07 15:19:30" ontape -r # answers as in step 5; the PIT is read from the environment $ onmode -m
Put RESTORE_FILTER back to your normal value (/usr/bin/gunzip) afterwards; with LW_PIT unset the PIT filter is transparent anyway. The point in time is read in the local time zone of the ontape process and is compared with the commit times the engine wrote into the log (see "Which clock" above). The filter prints what it did to stderr, which appears in ontape's output:
[logwalker] pit-filter: not a log backup stream (archive) — passed through untouched. [logwalker] pit-filter: not a log backup stream (archive) — passed through untouched. [logwalker] pit-filter: log 10: cut at the PIT. last commit 2026-10-07 15:19:26 commits kept 207 (of 208 seen before the first later commit) [logwalker] pit-filter: log 11: after the PIT — emitted with no log pages (nothing applied).
A real run. Level-0 archive written through gzip, then rows 1–3 committed, the point in time, rows 4–6 in the same log, and rows 7–9 in the next log (log 11); both logs backed up through gzip. With the filter above and LW_PIT between row 3 and row 4, ontape -r rolled forward logs 10 and 11 and the table held 1 2 3. The same restore with LW_PIT unset gave 1 2 3 4 5 6 7 8 9, identical to a plain restore. (The filter binary was run inside the 14.10 container by shipping the build host's own glibc with it; see Notes.)
- Notes. The filter must be a binary the engine's user can run on the database server, built for that server's glibc (the product's el8/el9 packages are); --restore-filter is the decoder that matches your BACKUP_FILTER, and only gzip has been run against the engine.
- An empty output for a log after the point in time also works, but ontape then prints “Unexpected end of log tape (errno 0), continuing…”; the no-log-pages form restores silently, which is why it is used.
- Logs are buffered in memory while the filter works (a log is at most LOGSIZE), archives are not.
- The tool never modifies a backup; the PIT lives only in the environment of the one ontape -r that you run.
What to know before you rely on it
- The archive must come before the cut. The level-0 (and any level-1/2) archive's log position must be earlier than the point in time. logwalker does not check this, so verify it yourself (for example from ontape's archive information when you start the restore).
- Work on a copy. --truncate-at only accepts --chunk (an offline file) and never writes in place; you can't point it at a live logical-log dbspace.
- Several commits in the same second are all kept. The cut is after the last commit at or before the time. Anything between that commit and the next one belongs to open transactions, which logical restore rolls back.
- Not covered yet: a transaction that was PREPAREd (XA) before the cut and committed after it is left in-doubt, not rolled back; a very large open transaction needs enough free log space to roll back (watch LTXHWM); and work done in unlogged tables or with logging turned off cannot be recovered to a point in time.
- What has been tested. On IDS 14.10.FC13W7, each with a real ontape level-0 archive and logical restore: a cut in the middle of a log (hundreds of pages dropped) with a transaction open across the point in time, a burst of 20 commits inside the point-in-time second (all kept), and a large bulk insert after it (not restored); a point in time on a log boundary (the earlier log restored whole, the later log left out); both LTAPEBLK 32 and 16; and the later-log case above. Every cut position was also checked against the engine's own onlog -l at 28 different commit times of one log.
- Informix versions. Verified against IDS 14.10.FC13W7 (and the 14.10.FC13W3 fixtures). The 15.x log page format is different and is not yet supported by logwalker.