Advanced Bucket Accessors in Couchbase make it possible to access advanced key-value store (KV) functionality using the following built-in operators.
They utilize the same bucket bindings defined in the handler as Basic Bucket Accessors, but expose a richer set of options and operators that can be used to:
- Set or retrieve expirations
- Solve race conditions via CAS
- Manipulate hot KV items under high contention
Note that Basic Bucket Accessors are much easier to use, have a trivial API, and are also a bit faster than the corresponding Advanced Bucket Accessors.
In Couchbase Server 6.6.1 the following Advanced Bucket Accessors have been added:
These seven new Bucket Accessors make it possible to utilize and leverage CAS directly to handle contention and/or set a document expiration (or TTL) in the Data Service (or KV) via Eventing plus perform distributed atomic counter operations.
For example, rather than blindly relying on Basic Bucket Accessors for an upsert like operation src_bkt[id_str] = some_doc
, the Advanced Accessors allow you to resolve contention (or possible contention) on keys that have concurrent mutations form different sources with JavaScript-driven logic in your Eventing Function.
-
- If the document doesn’t exist you can use
couchbase.insert(src_bkt, {"id: id_str}, some_doc)
and check the return value for success - If the document exists you can use
couchbase.replace(src_bkt, {"id: id_str, "cas": current_cas}, some_doc)
and check the return value for success or a CAS mismatch.
- If the document doesn’t exist you can use
To view complete examples including JavaScript, input mutations, output mutations and/or log messages for each Advanced Bucket Accessor, refer to Scriptlets: Advanced Accessor Handlers in the examples section of the documentation.
Advanced GET: result = couchbase.get(binding, meta)
This operation allows reading a document along with metadata from the bucket and subsequent operations to utilize CAS or check/modify the expiry_date
.
Contrast this to the Basic Bucket Accessor GET
operation which merely exposes a JavaScript binding or map, var adoc = src_bkt[meta.id]
, where the return value is just the document without any metadata.
Below is an example of the Advanced GET
operation.
1 2 3 4 5 6 7 8 9 10 11 12 |
function OnUpdate(doc, meta) { log('input doc ', doc); log('input meta', meta); // could be the same or different var new_meta = {"id":"test_adv_get::1"}; var result = couchbase.get(src_bkt,new_meta); if (result.success) { log('success adv. get: result',result); } else { log('failure adv. get: id',new_meta.id,'result',result); } } |
Some example return values:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
{ "doc": { "id": 1, "type": "test_adv_get" }, "meta": { "id": "test_adv_get::1", "cas": "1610034762747412480", "data_type": "json" }, "success": true } { "doc": { "a": 1, "random": 0.09799092443129842 }, "meta": { "id": "test_adv_insert:1", "cas": "1610140272584884224", "expiry_date": "2021-01-08T21:12:12.000Z", "data_type": "json" }, "success": true } { "error": { "code": 272, "name": "LCB_KEY_ENOENT", "desc": "The document key does not exist on the server", "key_not_found": true }, "success": false } |
Advanced INSERT: result = couchbase.insert(binding, meta, doc)
This operation allows creating a fresh document in the bucket. This operation will fail if the document with the specified key already exists. It allows specifying an expiration time (or TTL) to be set on the document.
There is no analogous Basic Bucket Accessor operation to the Advanced INSERT
operation (as src_bkt[meta.id] = adoc
is more like an upsert).
Below is an example of the Advanced INSERT
operation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function OnUpdate(doc, meta) { log('input meta', meta); log('input doc ', doc); // could be the same or different var new_meta = {"id":"test_adv_insert:1"}; // optional set an expiry 60 seconds in the future // new_meta.expiry_date = new Date(Date.now() + 60 * 1000); var new_doc = doc; new_doc.random = Math.random(); var result = couchbase.insert(src_bkt,new_meta,new_doc); if (result.success) { log('success adv. insert: result',result); } else { log('failure adv. insert: id',new_meta.id,'result',result); } } |
Some example return values:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{ "meta": { "id": "test_adv_insert:1", "cas": "1610041053310025728" }, "success": true } { "error": { "code": 272, "name": "LCB_KEY_EEXISTS", "desc": "The document key already exists in the server.", "key_already_exists": true }, "success": false } |
Advanced UPSERT: result = couchbase.upsert(binding, meta, doc)
This operation allows updating an existing document in the bucket, or if absent, creating a fresh document with the specified key. The operation does not allow specifying CAS (it will be silently ignored). It also allows specifying an expiration time (or TTL) to be set on the document.
Contrast this to the Basic Bucket Accessor SET
operation which merely uses an exposed JavaScript map defined via a bucket binding alias src_bkt[meta.id] = adoc
. For the basic SET
operation there is no return value (no status and no metadata) thus no way to check the CAS value.
Below is an example of the Advanced UPSERT
operation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function OnUpdate(doc, meta) { log('input meta', meta); log('input doc ', doc); // could be the same or different var new_meta = {"id":"test_adv_upsert:1"}; // CAS if supplied will be ignored // optional set an expiry 60 seconds in the future // new_meta.expiry_date = new Date(Date.now() + 60 * 1000); var new_doc = doc; new_doc.random = Math.random(); var result = couchbase.upsert(src_bkt,new_meta,new_doc); if (result.success) { log('success adv. upsert: result',result); } else { log('failure adv. upsert: id',new_meta.id,'result',result); } } |
An example return value:
1 2 3 4 5 6 7 |
{ "meta": { "id": "test_adv_upsert:1", "cas": "1610127444908376064" }, "success": true } |
Advanced REPLACE: result = couchbase.replace(binding, meta, doc)
This operation replaces an existing document in the bucket. This operation will fail if the document with the specified key does not exist. This operation allows specifying a CAS value that must be matched as a pre-condition before proceeding with the operation. It also allows specifying an expiration time (or TTL) to be set on the document.
There is no analogous Basic Bucket Accessor operation to the Advanced REPLACE
operation (as src_bkt[meta.id] = adoc
is more like an upsert).
Below is an example of the Advanced REPLACE
operation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
function OnUpdate(doc, meta) { log('input meta', meta); log('input doc ', doc); var mode = 3; // 1-> no CA, 2-> mismatch in CA, 3-> good CAS // Setup, make sure we have our doc to "replace", ignore any errors couchbase.insert(src_bkt,{"id":"test_adv_replace:10"},{"a:": 1}); var new_meta; if (mode === 1) { // If we pass no CAS it will succeed new_meta = {"id":"test_adv_replace:10"}; // optional set an expiry 60 seconds in the future // new_meta.expiry_date = new Date(Date.now() + 60 * 1000); } if (mode === 2) { // If we pass a non-matching CAS it will fail, so test this new_meta = {"id":"test_adv_replace:10", "cas":"1111111111111111111"}; } if (mode === 3) { // If we pass the matching or current CAS it will succeed var tmp_r = couchbase.get(src_bkt,{"id":"test_adv_replace:10"}); if (tmp_r.success) { // Here we use the current CAS just read via couchbase.get(...) new_meta = {"id":"test_adv_replace:10", "cas": tmp_r.meta.cas}; } else { log('Cannot replace non-existing key that create it and rerun',"test_adv_replace:10"); return; } } var new_doc = doc; new_doc.random = Math.random(); var result = couchbase.replace(src_bkt,new_meta,new_doc); if (result.success) { log('success adv. replace: result',result); } else { log('failure adv. replace: id',new_meta.id,'result',result); } } |
Some example return values:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{ "meta": { "id": "test_adv_replace:10", "cas": "1610130177286144000" }, "success": true } { "error": { "code": 272, "name": "LCB_KEY_EEXISTS", "desc": "The document key exists with a CAS value different than specified", "cas_mismatch": true }, "success": false } |
Advanced DELETE: result = couchbase.delete(binding, meta)
This operation allows deleting a document in the bucket specified by key. Optionally, a CAS value may be specified which will be matched as a pre-condition to proceed with the operation.
Contrast this to the Basic Bucket Accessor DEL
operation which merely uses an exposed a JavaScript binding or map, delete src_bkt[meta.id]
, where there is no return value (no status and no metadata).
Below is an example of the Advanced DELETE
operation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
function OnUpdate(doc, meta) { log('input meta', meta); log('input doc ', doc); var mode = 4; // 1-> no CA, 2-> mismatch in CA, 3-> good CAS, 4-> no such key // Setup, make sure we have our doc to "delete", ignore any errors couchbase.insert(src_bkt,{"id":"test_adv_delete:10"},{"a:": 1}); var new_meta; if (mode === 1) { // If we pass no CAS it will succeed new_meta = {"id":"test_adv_delete:10"}; // optional set an expiry 60 seconds in the future // new_meta.expiry_date = new Date(Date.now() + 60 * 1000); } if (mode === 2) { // If we pass a non-matching CAS it will fail, so test this new_meta = {"id":"test_adv_delete:10", "cas":"1111111111111111111"}; } if (mode === 3) { // If we pass the matching or current CAS it will succeed var tmp_r = couchbase.get(src_bkt,{"id":"test_adv_delete:10"}); if (tmp_r.success) { // Here we use the current CAS just read via couchbase.get(...) new_meta = {"id":"test_adv_delete:10", "cas": tmp_r.meta.cas}; } else { log('Cannot delete non-existing key that create it and rerun',"test_adv_delete:10"); return; } } if (mode === 4) { // Remove so that we have: no such key delete src_bkt["test_adv_delete:10"] new_meta = {"id":"test_adv_delete:10"}; } var result = couchbase.delete(src_bkt,new_meta); if (result.success) { log('success adv. delete: result',result); } else { log('failure adv. delete: id',new_meta.id,'result',result); } } |
Some example return values:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
{ "meta": { "id": "key::10", "cas": "1609374065129816064" }, "success": true } { "error": { "code": 272, "name": "LCB_KEY_EEXISTS", "desc": "The document key exists with a CAS value different than specified", "cas_mismatch": true }, "success": false } { "error": { "code": 272, "name": "LCB_KEY_ENOENT", "desc": "The document key does not exist on the server", "key_not_found": true }, "success": false } |
Advanced INCREMENT: result = couchbase.increment(binding, meta)
This operation atomically increments the field count
in the specified document. The document must have the below structure:
1 |
{"count": 23} // 23 is the current counter value |
The increment
operation returns the post-increment value.
If the specified counter document does not exist, one is created with count
value as 0 and the structure noted above. And so, the first returned value will be 1.
Due to limitations in KV engine API, this operation cannot currently manipulate full document counters.
There is no analogous Basic Bucket Accessor operation to the Advanced INCREMENT
operation.
Below is an example of the Advanced INCREMENT
operation.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function OnUpdate(doc, meta) { log('input meta', meta); log('input doc ', doc); // if doc.count doesn't exist it will be created var ctr_meta = {"id": "my_atomic_counter:1" }; var result = couchbase.increment(src_bkt,ctr_meta); if (result.success) { log('success adv. increment: result',result); } else { log('failure adv. increment: id',ctr_meta.id,'result',result); } } |
An example return value, assume you create this KEY "my_atomic_counter:1" DOC {"count": 23}
if the Eventing function above is deployed the count will be immediately incremented:
1 2 3 4 5 6 7 8 9 10 |
{ "doc": { "count": 24 }, "meta": { "id": "key::1", "cas": "1609374571840471040" }, "success": true } |
Advanced DECREMENT: result = couchbase.decrement(binding, meta)
This operation atomically decrements the field count
in the specified document. The document must have the below structure:
1 |
{"count": 23} // 23 is the current counter value |
The decrement
operation returns the post-decrement value.
If the specified counter document does not exist, one is created with the count
value as 0 and the structure noted above. As a result, the first returned value will be -1.
Due to limitations in KV engine API, this operation cannot currently manipulate full document counters.
There is no analogous Basic Bucket Accessor operation to the Advanced DECREMENT
operation.
Below is an example of the Advanced DECREMENT
operation.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function OnUpdate(doc, meta) { log('input meta', meta); log('input doc ', doc); // if doc.count doesn't exist it will be created var ctr_meta = {"id": "my_atomic_counter:1" }; var result = couchbase.decrement(src_bkt,ctr_meta); if (result.success) { log('success adv. decrement: result',result); } else { log('failure adv. decrement: id',ctr_meta.id,'result',result); } } |
An example return value, assume you create this KEY "my_atomic_counter:1" DOC {"count": 23}
if the Eventing function above is deployed the count will be immediately decremented:
1 2 3 4 5 6 7 8 9 10 |
{ "doc": { "count": 22 }, "meta": { "id": "key::1", "cas": "1609374770297176064" }, "success": true } |
References