Streaming / CDC Firebird (fb_streaming)
fb_streaming — Firebird replication log streaming
fb_streaming is a service (Windows) or daemon (Linux) that reads archived Firebird replication log segments, decodes them into events, and gives the events to a plugin. The plugin sends committed changes to an external system: JSON files, Kafka, RabbitMQ, MongoDB, or Lucene full-text indexes. The database and its applications do not change. The service works only with the replication journal. Main description: fb_streaming.md.
Works on archived segments after Firebird writes them. No triggers, no load on user transactions.
Changes are buffered per transaction and savepoint. Only committed work is published.
A control file stores the position and open transactions. After a restart, the service replays them.
One service instance runs many tasks. Each task has its own thread and its own source folder.
Plugins are shared libraries (
.dll / lib*.so). You can write your own in C++ or Delphi.fb_repl_print prints any segment in readable form, with record values and offsets.2. Data flow
Firebird writes changes of published tables to the replication journal. When a segment is full (or after
journal_archive_timeout), Firebird archives it. The journal_archive_command can copy the
archived segment to an extra folder. Each fb_streaming task watches one folder. See “How it works?” in fb_streaming.md.
- Scan the task folder for unprocessed segments (state
archive). - Parse each segment block by block and raise events.
- The plugin buffers transactional events and publishes them on
COMMIT. - Write the new position to the control file.
- Delete segments that are no longer needed (if
deleteProcessedFile = true). - On error: log it, wait
errorTimeoutseconds (60 by default), rescan, restart the task.
- Firebird 4.0 or higher (HQbird) with replication publication on.
- Archived segments in the task folder. Active segments are not read.
- A database connection (
database,username,password). The service reads table record formats to decode records. - The plugin library in
stream_plugins(orpluginDir).
3. Replication events
The parser raises these events. Each plugin decides which events it uses and which it ignores. The same list is in fb_streaming.md and fb_repl_print.md.
| Group | Events | Notes |
|---|---|---|
| Transaction | START, PREPARE (2PC phase 1), COMMIT, ROLLBACK | Each has a transaction number (tnx). A transaction can start in one segment and end in another. |
| Savepoint | SAVE, RELEASE, UNDO (rollback to savepoint) | Firebird creates savepoints for each statement, so you see them often. |
| Data | INSERT, UPDATE, DELETE | INSERT has the new record. UPDATE has old and new records. DELETE has the old record. |
| BLOB | BLOB (store BLOB) | Written before the record that points to it. Only new BLOB values are in the log. |
| Other | EXECUTE SQL, SET SEQUENCE | EXECUTE SQL appears only for DDL. SET SEQUENCE has no transaction: generators work outside transactions. |
4. Transaction buffering (interactive)
The message plugins (Kafka, RabbitMQ, MongoDB, Lucene) keep one buffer per active transaction. Inside the buffer there is a stack of savepoints. Documents go to the top savepoint. RELEASE moves documents one level down. UNDO drops the level and its documents. COMMIT sends everything. ROLLBACK drops everything. SET SEQUENCE does not wait: it is sent at once. Algorithms: rabbitmq_plugin.md, mongodb_events_plugin.md, fts_lucene_plugin.md.
5. Control file and crash recovery
For each task the service keeps a control file named <database GUID>. It is created in
controlFileDir (default: the task folder). Source: fb_streaming.md. It holds:
- control data (database GUID, size, and so on);
- the last processed segment and the position in it;
- the segment with the oldest active transaction;
- the list of active transactions as pairs
{tnxNumber, segmentNumber}(segment where each started).
Delete rule: a segment is deleted only when its number is lower than the segment of the oldest active transaction. Restart rule: after a crash, the service rereads segments from the oldest-active-transaction segment up to the last processed one, replays the events of the open transactions, and then goes on as usual.
6. Configuration
replication.conf (Firebird side)
Turn on the journal and archive for the database. A task folder must be used by only one task, and processed segments are deleted. So, if you also run a replica or more tasks, copy each archived segment to one folder per consumer:
database = d:\fbdata\5.0\test.fdb
{
journal_directory = d:\fbdata\5.0\replication\test\journal
journal_archive_directory = d:\fbdata\5.0\replication\test\archive
journal_archive_command = "copy $(pathname) $(archivepathname) && copy $(pathname) d:\fbdata\5.0\replication\test\kafka_source"
journal_archive_timeout = 10
}
If the journal is used only for streaming, set journal_archive_directory straight to the task folder and omit the command.
Then publish the tables:
ALTER DATABASE INCLUDE ALL TO PUBLICATION; -- or: INCLUDE CUSTOMERS TO PUBLICATION
ALTER DATABASE ENABLE PUBLICATION;
fb_streaming.conf (service side)
The file sits next to the executable. Global settings come first, then one task block per source folder.
Full parameter list: fb_streaming.md (“Configuring the service”) and the plugin documents below.
# logLevel = info # trace, debug, info, warning, error, critical, off
# pluginDir = $(root)/stream_plugins # $(root) = service folder
task = d:\fbdata\5.0\replication\test\kafka_source
{
database = inet://localhost:3055/test
username = SYSDBA
password = masterkey
plugin = kafka_cdc_plugin
deleteProcessedFile = true
# controlFileDir = ... # default: the task folder
# errorTimeout = 60
# plugin-specific parameters follow
}
| Parameter | Scope | Meaning | Default |
|---|---|---|---|
logLevel | global | Log level of the service | info |
pluginDir | global | Folder with plugin libraries | $(root)/stream_plugins |
database * | task | Connection string to the source database | — |
username, password | task | Credentials for the connection | — |
plugin * | task | Plugin name (name.dll / libname.so) | — |
controlFileDir | task | Where the control file lives | task folder |
errorTimeout | task | Seconds to wait after an error before restart | 60 |
deleteProcessedFile | task | Delete segments after processing (set false to debug) | true |
warn_tnx_not_found | task (Kafka) | Warn and skip, instead of error, if a transaction start is missing | false |
* required. Common plugin filters: include_tables / exclude_tables (regular expressions),
dumpBlobs, register_ddl_events, register_sequence_events, charset_for_none_fields.
Install and run
fb_streaming install [service_name]
fb_streaming start [service_name]
fb_streaming stop [service_name]
fb_streaming remove [service_name]
Several services on one host need unique names. Run fb_streaming with no arguments to test it as a console app (Enter stops it).sudo systemctl enable fb_streaming
sudo systemctl start fb_streaming
sudo systemctl stop fb_streaming
sudo systemctl disable fb_streaming
Kafka plugin on Linux needs librdkafka1 and liblz4-1 from the package manager (see kafka_cdc_plugin.md, “Requirements”).7. Plugins
| Plugin | Target | Format | Publishes | When |
|---|---|---|---|---|
simple_json_plugin | Folder (outputDir) | One .json per segment | All events, incl. savepoints | Per segment |
kafka_cdc_plugin | Kafka topics | Debezium-like JSON, optional schema | INSERT / UPDATE / DELETE (+ tx BEGIN/END) | On COMMIT |
rabbitmq_plugin | RabbitMQ exchange | JSON message per event | DML, DDL, SET SEQUENCE | On COMMIT (sequences at once) |
mongodb_events_plugin | MongoDB events collection | BSON document per event | DML, DDL, SET SEQUENCE | On COMMIT (sequences at once) |
fts_lucene_plugin | Lucene FTS indexes (UDR) | Index documents | DML on indexed tables | On COMMIT |
simple_json_plugin — segment to JSON file
Demo plugin shipped with the service. It turns each binary segment into a JSON file with the same name plus .json.
Full description: simple_json_plugin.md. The root object has header (guid, sequence, state, version) and an events array.
{
"header": { "version": 1, "sequence": 1, "state": "archive",
"guid": "{F396449D-F6E4-4812-875E-248AB7C2BEE7}" },
"events": [
{ "event": "START TRANSACTION", "tnx": 1937 },
{ "event": "SAVEPOINT", "tnx": 1937 },
{ "event": "UPDATE", "table": "CUSTOMER", "tnx": 1937,
"changedFields": ["ADDRESS"],
"oldRecord": { "CUSTOMER_ID": 8, "ADDRESS": null, ... },
"record": { "CUSTOMER_ID": 8, "ADDRESS": "555", ... } },
{ "event": "RELEASE SAVEPOINT", "tnx": 1937 },
{ "event": "COMMIT", "tnx": 1937 }
]
}
event,tnx(not for SET SEQUENCE)table,record,oldRecord,changedFields— DMLsql— EXECUTE SQLsequence,value— SET SEQUENCEblobId,data(hex) — STORE BLOB
outputDir (required), dumpBlobs, register_ddl_events, register_sequence_events,
include_tables, exclude_tables, charset_for_none_fields.
kafka_cdc_plugin — Change Data Capture to Kafka
Writes row-level change events in a format close to Debezium. Events are buffered per transaction and sent on commit.
Each event has a key (primary key, or unique key if no PK) and a value (an envelope with before / after).
Full description with schemas, all kafka_* options and a Docker Kafka setup: kafka_cdc_plugin.md.
{ "before": null,
"after": { "ID": 1, "FIRST_NAME": "Anne", "LAST_NAME": "Kretchmar", "EMAIL": "[email protected]" },
"source": { "dbguid": "{9D66A972-...}", "sequence": 1, "filename": "TEST.FDB.journal-000000001",
"table": "CUSTOMERS", "tnx": 200, "ts_ms": 1711288254908 },
"op": "c", "ts_ms": 1711288255056 }{ "before": { "ID": 1, "FIRST_NAME": "Anne", ... },
"after": { "ID": 1, "FIRST_NAME": "Anne Marie", ... },
"source": { ..., "sequence": 2, "tnx": 219 },
"op": "u", "ts_ms": 1711288256121 }{ "before": { "ID": 1, "FIRST_NAME": "Anne Marie", ... },
"after": null,
"source": { ..., "sequence": 3, "tnx": 258 },
"op": "d", "ts_ms": 1711288256152 }// topic $[topicPrefix].transaction (transaction_metadata_enable = true)
{ "status": "BEGIN", "id": 901, "ts_ms": 1713099401529, "event_count": null, "data_collections": null }
{ "status": "END", "id": 901, "ts_ms": 1713099404605, "event_count": 2,
"data_collections": [ { "data_collection": "fbstreaming.cdc.CUSTOMERS", "event_count": 2 } ] }
// each change event then also gets:
"transaction": { "id": 901, "total_order": 1, "data_collection_order": 1 }kafka_topic_prefixsets$[topicPrefix](defaultfb_streaming).kafka_topic_cdc_event=$[topicPrefix].cdc→ all tables in one topic.$[topicPrefix].cdc.$[tableName]→ one topic per table.kafka_topic_transaction=$[topicPrefix].transaction.
kafka_brokers— comma-separated list.key_cdc_events_enable— send keys (else key is null).key_converter_schemas_enable,value_converter_schemas_enable.transaction_metadata_enable— BEGIN/END events.async_producer— do not wait for delivery ack (buffer 500).kafka_*— librdkafka options, dots replaced by_: SSL, SASL (64-bit only), OAUTHBEARER/OIDC.
Firebird → Kafka type mapping
| Firebird type | JSON type | Notes |
|---|---|---|
| BOOLEAN | boolean | |
| SMALLINT / INTEGER / BIGINT | int16 / int32 / int64 | |
| FLOAT / DOUBLE PRECISION | float / double | |
| INT128, NUMERIC, DECIMAL, DECFLOAT | string | Exact value as text |
| CHAR, VARCHAR | string | NONE charset: see charset_for_none_fields |
| BINARY, VARBINARY | string | Hex, two chars per byte |
| DATE / TIME / TIMESTAMP [WITH TIME ZONE] | string | Y-M-D, HH24:MI:SS.F (F = 1/10000 s), plus zone name |
| BLOB SUB_TYPE TEXT / 0 | string | before is always null; binary as hex |
rabbitmq_plugin — events to a RabbitMQ exchange
Each event is a JSON message sent to exchange_name. With exchange_type = fanout (default) every bound queue
gets it; with direct / topic the routing_key decides. If queue_name is set, the queue is declared and bound.
The exchange is created if it does not exist. Full description: rabbitmq_plugin.md.
task = d:\fbdata\4.0\replication\horses\archive_rabbit
{
database = inet://localhost:3054/d:\fbdata\4.0\horses.fdb
username = SYSDBA
password = masterkey
plugin = rabbitmq_plugin
rabbit_uri = amqp://guest:guest@localhost
exchange_name = horses
exchange_type = direct
queue_name = horses_events
routing_key = 123
dumpBlobs = true
}
{ "event": "UPDATE", "table": "COLOR", "tnx": 11771, "changedFields": ["NAME_DE"],
"oldRecord": { "CODE_COLOR": 14, "NAME_DE": "", ... },
"record": { "CODE_COLOR": 14, "NAME_DE": "g", ... } }
{ "event": "SET SEQUENCE", "sequence": "SEQ1", "value": 1 }
{ "event": "EXECUTE SQL", "sql": "CREATE SEQUENCE SEQ1", "tnx": 6590 }
mongodb_events_plugin — events to MongoDB
Same event model and same buffering as RabbitMQ. Each event becomes a document in the events collection of
mongo_database (created on first write). Parameters: mongo_uri (for example mongodb://localhost:27017),
mongo_database, plus the common filters. Full description: mongodb_events_plugin.md.
use horses
db.events.find()
{ _id: ObjectId("638f37f9022b0000ad005779"), event: 'INSERT', table: 'TABLE1', tnx: 6597, record: { ID: 1, S: 'Hello' } }
fts_lucene_plugin — keep full-text indexes current
Updates indexes made by IBSurgeon Full Text Search UDR. On each iteration the plugin rereads index metadata, takes only active indexes, and filters DML events by their tables. On COMMIT it adds, updates, or deletes index documents. Full description: fts_lucene_plugin.md.
New document per index. Skipped if all indexed fields are empty.
Skipped if no indexed field changed. Deleted from index if all indexed fields are now empty.
Key is saved; the document is removed on commit.
RDB$DB_KEY (not in the log) and indexes built on views.8. fb_repl_print utility
Prints a binary segment in readable form. Use it to check what a segment holds, or to find the record that broke replication. Full description: fb_repl_print.md.
fb_repl_print [file_path] <options>
-i, --input-file file segment file (or first argument)
-H, --print-header segment header: version, GUID, sequence, state, length
-B, --print-blobs BLOB contents in hex
-R, --dump-records record values as JSON (needs --database)
-F, --print-format record format: field names and types (needs -R)
-d, --database / -u, --username / -p, --password
-c, --charset-none-fields charset for NONE fields
BLOCK (offset: 48)
[200] START (offset: 64)
[200] SAVE (offset: 65)
[200] SAVE (offset: 66)
[200] INSERT CUSTOMERS (length: 3082) (offset: 78)
[200] RELEASE (offset: 3169)
[200] RELEASE (offset: 3170)
BLOCK (offset: 3171)
[] SET SEQUENCE RDB$2 = 1 (offset: 3194)
[200] COMMIT (offset: 3207)
[200]— transaction number.[]— outside a transaction.offset— byte offset in the segment file.- Records are stored in table format. To decode them,
-Rconnects to the database (UTF8) and readsRDB$FORMATS. If the table or format is gone (for example after DROP TABLE), it prints a warning. - Output is UTF8. On Windows run
chcp 65001first.
9. Writing your own plugin
A plugin is a shared library in C++ or Delphi. It implements three interfaces and exports the entry point
fb_stream_plugin, which registers a factory by name. Helper templates with the Impl suffix reduce boilerplate.
Full examples: IBSurgeon/FBStreamingExamples.
createPlugin(config, encodeUtils, logger)init, finish, startSegment, finishSegment, startBlock, setSegmentOffset, startTransaction, getTransaction, setSequence, matchTable, cleanupTransaction(s), logprepare, commit, rollback, start/release/rollbackSavepoint, insertRecord, updateRecord, deleteRecord, executeSql[Intl], storeBlobRecords come as IStreamedRecord (getCount, getField, raw data). Fields are IStreamedField
(getName, getType, getScale, getCharSet, getData, isKey, keyPosition, isNullable).
IStreamLogger writes to the service log. Every function is described in writing_plugin.md.
getTransaction(tnx) for transactions continued from an earlier segment, and cleanupTransaction(s) to free them.extern "C" FB_DLL_EXPORT void fb_stream_plugin(IMaster* master, IStreamPluginManager* pm)
{
auto factory = new MyPlugin::MyStreamPluginFactory(master);
pm->registerPluginFactory("my_plugin", factory); // plugin = my_plugin in fb_streaming.conf
}
10. Limits and rules
journal_archive_timeout to limit the delay.dumpBlobs publishes new values only.source.ts_ms is the segment file modification time.charset_for_none_fields to the real encoding of such columns.warn_tnx_not_found = true logs a warning and skips it.async_producer = true is faster but does not wait for broker confirmation (buffer 500 messages).Further reading
Source documents in this folder (Markdown):
| Document | Russian | Content |
|---|---|---|
| fb_streaming.md | RU | Service (daemon): how it works, control file, install, configuration |
| kafka_cdc_plugin.md | RU | Kafka CDC plugin: event format, schemas, type mapping, SSL/SASL options, Docker setup |
| rabbitmq_plugin.md | RU | RabbitMQ plugin: algorithm, exchange and queue settings, message examples |
| mongodb_events_plugin.md | RU | MongoDB plugin: algorithm, settings, events collection example |
| simple_json_plugin.md | RU | JSON plugin: file format and settings |
| fts_lucene_plugin.md | RU | Lucene full-text index plugin: algorithm and setup |
| fb_repl_print.md | RU | fb_repl_print utility: options, output, finding replication errors |
| writing_plugin.md | RU | Plugin SDK: all interfaces and functions (C++ / Delphi) |
| kafka_cdc_plugin.adoc | RU | Kafka CDC plugin, AsciiDoc version |
External: FBStreamingExamples (plugin source examples), lucene_udr (Full Text Search UDR), Debezium (event format reference).