Apache CouchDB CDC: Changes feed setup with Gluesync
Source data from Apache CouchDB
The Apache CouchDB source agent (agent tag couchdb) copies the databases you choose with a snapshot and then follows their _changes feed. It creates nothing in your databases and installs nothing on the server.
Databases, entities and fields
-
One entity is one CouchDB database. CouchDB has no level above the database, so the only schema is
_default; -
Discovery lists the databases with
GET /_all_dbs, without the system databases (every name that starts with_, such as_users,_replicatorand_global_changes). In CouchDB 3.x that endpoint is reserved to server administrators by default. When the server answers401or403, or when you fill the Databases field of the connection, the list is the one from the connection and each name is checked withGET /<database>; -
The fields of a database are inferred from a sample of its documents (Schema discovery sample size, default
1000): half of the sample comes from the start of_all_docsand half from its end. Design documents are left out of the sample. Names that start with_are CouchDB members (_rev,_attachments, …) and are never fields. The key of the entity is_id, aSTRINGthat is not nullable; -
The type of a field comes from the JSON kinds of its values, ignoring
null: a field that only holds integers (no fraction, no exponent, within 64 bits) isinteger, any other number isnumber, and a field with one other kind isstring,boolean,objectorarray. A field with more than one kind, or with nothing butnull, isjson. A field is nullable when a sampled document lacks it or holdsnull.
A field that appears only in documents outside the sample is not discovered. Raise Schema discovery sample size for a database with documents of different shapes.
How it works
Snapshot
The snapshot never pages _all_docs: that endpoint is bound to one stream on the server and does not scale with key ranges. The agent takes the ids from the changes feed instead, and reads the documents with many parallel _bulk_get requests:
producer (pages of ids) -> bounded channel of id chunks -> N workers (_bulk_get + mapping) -> bounded channel -> collector (emits)
-
The producer reads the ids from
_changesin pages of Snapshot page size (default10000), asking for ids only, which spares the server the sequence it would otherwise compute for every row. Chunks of Snapshot fetch batch size ids (default1000) go to Snapshot read concurrency workers (default8), each of which calls_bulk_getand maps the documents; -
Core Hub hands over the position CDC will start from: the one read before the first id was listed and, when an interrupted snapshot is continued, the one of its first run. Changes made while the snapshot runs are replayed by CDC from that position: a document read with its newer body is written again, a document not read yet is inserted, and a document that was deleted is deleted again. Keep the insert conflict strategy of the entity on its default, Upsert. See Insert conflict strategy;
-
Deleted documents and design documents are not read. A document that is gone when its chunk is fetched is skipped;
-
Both channels hold a handful of chunks, so a slow target slows the workers and then the producer down, and a database is never held in memory. A snapshot needs about
3 x Snapshot read concurrency x Snapshot fetch batch sizedocuments in memory: lower the batch size for very large documents. A message to Core Hub is cut bymaxTransactionMessageKbSizeand carries whole chunks; -
The row count of an entity is the
doc_countof the database minus its design documents. The snapshot ends at the end of the feed: a database that is written faster than it is read keeps the snapshot chasing the tail; -
Resume. The workers finish their chunks in any order, so the agent saves only the contiguous prefix of pages whose chunks all reached a message that was emitted, with the position of the feed at the end of that prefix. A resumed snapshot restarts the id listing at that position and counts on from those rows. The rows read beyond the prefix are read again, which the upsert absorbs. A position that the feed no longer knows (HTTP
400) fails the entity with "run a new snapshot"; -
A snapshot ignores the logical partitions of an entity, because it already reads in parallel. Asking Core Hub to compute or count the partitions fails with a message that says so.
Measured on CouchDB 3.5.1 (single node, Docker on Apple silicon), 2,000 ids per _bulk_get request:
| Way to read the documents | Documents per second |
|---|---|
|
28,000 (does not scale) |
|
22,000 |
|
81,000 |
|
94,000 |
Every read of the changes feed sets seq_interval, which spares the server a sequence per row. Listing ids only reaches 108,000 rows per second with it; reading documents from the feed with include_docs=true reaches 41,000 rows per second with it and 14,000 without it.
Through the source of the agent, reading 200,000 documents of about 1 KB from a database with 8 shards, a snapshot read concurrency of 1 gives 16,300 documents per second and the default of 8 gives 61,800. The reads in flight are the lever (Snapshot read concurrency), and the number of shards of the source database sets the ceiling. In a full pipeline, copying 10 million documents from CouchDB to PostgreSQL took 274 seconds end to end.
Change data capture
A poll of an entity is GET /<database>/_changes in longpoll mode, from the position of the last change read, with include_docs=true, style=main_only, and a limit and a seq_interval of Changes batch size rows (default 5000). Core Hub asks for the last position of the database on every tick and polls only when it differs from the one it holds. A long poll that finds nothing answers empty after Changes long poll timeout (ms) (default 1000).
-
The position of an entity is the opaque
seqof its changes feed. In a cluster, a sequence encodes one position per shard. The agent orders positions by the number before the first dash (the sum of the sequences of the shards), the only part that is the same in every spelling CouchDB uses; -
Insert, update or delete. A row of the feed marked
deletedis a delete. Any other document is an insert when its revision generation is1(it was created and not modified since) and an update otherwise. The agent sends the whole document: CouchDB keeps no before image; -
Deletes carry only the
_id. A delete has every column of the entity, with a value only for the key, which is_id. A delete cannot name any other key: on an entity keyed on more than_idthe delete is skipped, and the agent logs it. Keep_idas the key of the entity; -
Design documents are skipped. Ids that start with
_design/are not replicated, in the snapshot and in CDC; -
Delivery is at least once. A page is emitted as one or more messages cut by size. Every message but the last carries the position the page started from, and only the last carries the position at the end of the page, so a crash in between replays the page. The target applies a change twice without harm: an upsert and a delete that tolerates a missing document;
-
A backlog is read in one go. A poll keeps reading while the pages come back full, up to ten pages: only its first page waits for a change, so catching up is not limited to one page per polling interval;
-
Failures. A network failure, a timeout and the answers
408,429and5xxare asked again before the entity fails.404fails the entity with the name of the database.400on the position fails it with "run a new snapshot".
Prerequisites
To have Gluesync working on your Apache CouchDB instance as a source you will need:
-
Apache CouchDB 3.3, 3.4 or 3.5, a single node or a cluster;
-
A user that is a member of every database to replicate, with read access to its documents. In CouchDB 3.x the database
_securityobject lists the members; a database without members is open to every authenticated user. Reading the changes feed and the documents is all the agent does; -
To list the databases, a server administrator.
GET /_all_dbsis reserved to administrators by default in CouchDB 3.x. With a non-administrator user, put the names of the databases in the Databases field of the connection, separated by commas; -
Network access from Core Hub to the HTTP port of the node (default
5984), or to the TLS port you use.
To create a user that can read a database, you can follow this quick guide, with an administrator account:
# create the user
curl -u admin:password -X PUT http://couchdb-host:5984/_users/org.couchdb.user:gluesync \
-H 'Content-Type: application/json' \
-d '{"name": "gluesync", "password": "your_password", "roles": [], "type": "user"}'
# make it a member of the database to replicate
curl -u admin:password -X PUT http://couchdb-host:5984/db_name/_security \
-H 'Content-Type: application/json' \
-d '{"admins": {"names": [], "roles": []}, "members": {"names": ["gluesync"], "roles": []}}'
Run the second request for every database to replicate. The _security request replaces the whole object: add the user to the members the database already has.
Setup via Web UI
-
Protocol:
HTTP REST, the only one; -
Node address: DNS record or IP address of one node of the server, without the scheme (
couchdb.example.com, nothttp://couchdb.example.com); -
Port: Optional, defaults to
5984; -
Username: Username with the access listed in the prerequisites;
-
Password: Password belonging to the given username;
-
Disable authentication: Optional, defaults to
false. Sends the requests with no authorization, for a server that allows anonymous access; -
Enable TLS: Optional, defaults to
false. When it is on, the node URLs usehttps://and the server certificate is always validated. There is no option to trust it blindly; -
CA certificate path and its password: Optional. A PKCS12 or JKS store, or a PEM file, that holds the certificate authority of the server. The certificate can also be uploaded through Core Hub. Blank means the default trust store of the JVM. A server with a certificate from a private CA needs this;
-
Additional nodes: Optional.
host1:port1,host2:port2, the other nodes of a cluster, tried in order when a node is down. A node without a port takes the port above; -
Databases (comma separated): Optional. The databases to offer when the user may not list them, for example
orders,customers.
Custom properties
-
Path prefix: (optional, defaults to empty) The path the server answers under, when it sits behind a reverse proxy, for example
/couchdb; -
Connect timeout (ms): (optional, defaults to
10000) How long a node may take to accept a connection before the next one is tried; -
Request timeout (ms): (optional, defaults to
300000) How long a request may run, long polls and big requests included.
Specific configuration
| Setting | Default | Description |
|---|---|---|
Source change retention in hours ( |
|
Common setting of the source agents, see Source change retention in hours. This agent reads the changes feed of each entity directly and keeps no cache of changes of its own. |
Schema discovery sample size ( |
|
Documents sampled to infer the fields of a database: half from the start of |
Snapshot read concurrency ( |
|
Parallel |
Snapshot fetch batch size ( |
|
Document ids in one |
Snapshot page size ( |
|
Rows of one |
Changes batch size ( |
|
Rows of one |
Changes long poll timeout (ms) ( |
|
How long a poll of the change feed waits for a change before it answers empty. |
Technical fields ( |
none |
Adds |
Setup via Rest APIs
Here following an example of calling the Core Hub’s Rest API via curl to setup the connection for this Agent.
Connect the agent
curl --location --request PUT 'http://core-hub-ip-address/pipelines/{pipelineId}/agents/{agentId}/config/credentials' \
--header 'Content-Type: application/json' \
--header 'Authorization: ••••••' \
--data '{
"agentType": "SOURCE",
"agentTag": "couchdb",
"hostCredentials": {
"connectionName": "myAgentNickName",
"host": "couchdb-host-address",
"port": 5984,
"username": "gluesync",
"password": "your_password",
"disableAuth": false,
"enableTls": false,
"additionalHosts": "couchdb-node2:5984,couchdb-node3:5984",
"databaseName": "orders,customers"
},
"customHostCredentials": {
"pathPrefix": "",
"connectTimeoutMilliseconds": 10000,
"requestTimeoutMilliseconds": 300000
}
}'
certificatePath and certificatePassword go in hostCredentials when TLS needs a private certificate authority. additionalHosts and databaseName are optional.
Setup specific configuration
curl --location --request PUT 'http://core-hub-ip-address/pipelines/{pipelineId}/agents/{agentId}/config/specific' \
--header 'Content-Type: application/json' \
--header 'Authorization: ••••••' \
--data '{
"configuration": {
"schemaDiscoverySampleSize": 1000,
"snapshotReadConcurrency": 8,
"snapshotFetchBatchSize": 1000,
"snapshotPageSize": 10000,
"changesBatchSize": 5000,
"changesLongPollTimeoutMilliseconds": 1000
}
}'
Data types
The agent reads the type of each field from the sample of documents, as described in Databases, entities and fields. A field that is missing from a document is reported as absent, and the absent field policy of the entity (absentFieldPolicy) decides what the target receives.
| CouchDB type | Gluesync type | Notes |
|---|---|---|
|
|
|
|
|
A value that is not an exact 64-bit integer fails the row, naming the field. Discovery picks |
|
|
Built from the literal text of the JSON number, so it is as exact as the stored JSON. CouchDB itself stores a decimal as an IEEE double, so |
|
|
|
|
|
Converted recursively: strings, integers, decimals and booleans, |
|
|
The same conversion, element by element. |
|
|
The JSON text of the value. The type of a field that mixes more than one JSON kind, or that holds only |
Notes
-
Attachments are neither read nor written. The
_attachmentsmember of a document is not a field; -
A purged document (
_purge) leaves no change in the feed, so the agent cannot delete it on the target; -
Partitioned databases are read as normal ones: the agent uses no partition-specific API;
-
The agent has no recursion protection for a CouchDB to CouchDB pipeline;
-
In a cluster, the feed of a database is not in commit order across shards. Each document appears once, with its winning revision, so the target ends with the same documents.
Troubleshooting
The server answers 401 or 403
401 is shown as "Authentication Failed": check the username and the password, or turn Disable authentication on for a server that allows anonymous access. 403 is shown as "Permission Denied": the user is not a member of the database. Add the user to the members of the database in its _security object.
No database is listed
The user cannot call _all_dbs, which in CouchDB 3.x needs a server administrator. Put the names of the databases in the Databases (comma separated) field of the connection: the agent lists them and checks each one with GET /<database>.
The database is not found
404 is shown as "Database Not Found", with the name of the database. The database was deleted or renamed, or the Path prefix is wrong. A name in the Databases field that is not a database also ends here.
CDC says to run a new snapshot
The position the entity holds is no longer known to the feed (HTTP 400). Start a new snapshot of the entity.
Deletes do not arrive on the target
A delete carries only the _id. On an entity whose key is made of more columns than _id the agent skips the delete and logs it. Make _id the key of the entity.