Docs · Context Transfer Engine
Using the Context Transfer Engine.
This page shows how to do each common job with the CTE client, in C++ and in Python. The API reference has the full signatures; the overview explains tags, blobs, scores and tiers.
- C++
- clio::cte::core::Tag, Client
- Python
- clio_cte_core_ext
- Default pool
- 512.0, or CLIO_CTE_POOL
Connect a client
Every program first initializes the runtime client, then the CTE client. The CTE client binds to the core pool 512.0 by default. The environment variable CLIO_CTE_POOL, as major.minor, binds it to another pool that speaks the core interface, such as the indexer at 564.0 or the cache at 563.0. Set it before initialization.
#include <clio_runtime/clio_runtime.h>
#include <clio_cte/core/core_client.h>
clio::run::CLIO_INIT(clio::run::RuntimeMode::kClient); // attach to a running clio_run
clio::cte::core::CLIO_CTE_CLIENT_INIT(); // bind the shared client
auto *client = CLIO_CTE_CLIENT; // clio::cte::core::Client *import os
os.environ["CLIO_WITH_RUNTIME"] = "0" # attach to a running clio_run
os.environ["CLIO_CTE_POOL"] = "564.0" # optional: the indexer, so SemanticSearch works
import clio_cte_core_ext as cte
cte.clio_init(cte.RuntimeMode.kClient, False) # (mode, default_with_runtime=False, is_restart=False)
cte.initialize_cte("", cte.PoolQuery.Dynamic()) # (config_path, pool_query)
client = cte.get_cte_client()To address a specific pool from C++ without the environment variable, construct clio::cte::core::Client with a PoolId. In Python, cte.Client() and cte.Client(pool_id) do the same.
Put and get a blob
The Tag wrapper is the simplest path. Its constructor runs GetOrCreateTag. The blocking PutBlob from a private buffer writes at offset 0 with score 1.0 unless told otherwise, and throws on a non-zero return code. GetBlob reads into a buffer you own.
clio::cte::core::Tag tag("experiment/run1");
std::string payload = "Arctic sea ice extent fell to a record low.";
tag.PutBlob("note0", payload.data(), payload.size()); // off 0, score 1.0
std::string out(tag.GetBlobSize("note0"), '\0');
tag.GetBlob("note0", out.data(), out.size());
tag.PutBlob("note0", "!", 1, /*off=*/payload.size()); // append one bytetag = cte.Tag("experiment/run1")
tag.PutBlob("note0", b"Arctic sea ice extent fell to a record low.", 0)
data = tag.GetBlob("note0", tag.GetBlobSize("note0"), 0) # bytesPython PutBlob has no score argument; a blob written from Python lands at 1.0 and can be moved afterwards with ReorganizeBlob. GetBlob raises RuntimeError when the read fails. A range whose bytes lived only in RAM before a runtime restart fails with an I/O error rather than returning zeros.
Work with tags
auto *client = CLIO_CTE_CLIENT;
const auto &id = tag.GetTagId();
std::vector<std::string> names = tag.GetContainedBlobs();
auto rn = client->AsyncRenameTag("experiment/run1", "experiment/run1-final", id);
rn.Wait();
auto sync = client->AsyncSyncTag(id); // flush to a persistent tier, like fsync
sync.Wait();
auto del = client->AsyncDelTag("experiment/run1-final");
del.Wait();names = tag.GetContainedBlobs()
tid = tag.GetTagId() # UniqueId with major_, minor_
later = cte.Tag(tid) # wrap an id, no RPC
fut = client.AsyncGetOrCreateTag("experiment/run2") # .result() -> UniqueId
run2 = cte.Tag(fut.result())The Python module exposes blob deletion (DelBlob) but not tag deletion, renaming or SyncTag.
Query by name
TagQuery and BlobQuery match whole names with std::regex_match: "doc.*" matches doc7, "doc" does not, and ".*ice.*" matches a substring. Both default to PoolQuery::Broadcast(), so every node answers for its own blobs and the results are merged. A max of 0 means no limit.
auto tq = client->AsyncTagQuery("experiment/.*");
tq.Wait();
for (const auto &name : tq->results_) { /* tag names */ }
auto bq = client->AsyncBlobQuery("experiment/.*", "note.*");
bq.Wait();
for (size_t i = 0; i < bq->blob_names_.size(); ++i) {
// bq->tag_names_[i], bq->blob_names_[i]
}tags = client.TagQuery("experiment/.*") # list[str]
for tag_name, blob_name in client.BlobQuery("experiment/.*", "note.*"):
print(tag_name, blob_name)Search by keyword
SemanticSearch is BM25 keyword search over blob contents. The core does not implement it; the indexer module at 564.0 does. The client must therefore be bound to the indexer or to a pool that forwards to it, through CLIO_CTE_POOL=564.0 or a Client constructed on that pool. Results come back best first, with score 0 for a blob that matched the patterns but contains no query word. k defaults to 10; 0 means no cap.
clio::cte::core::Client indexer(clio::run::PoolId(564, 0));
auto hits = indexer.AsyncSemanticSearch("experiment/.*", ".*", "sea ice", 5);
hits.Wait();
for (const auto &r : hits->results_) {
// r.tag_id_, r.blob_name_, r.score_
}for hit in client.SemanticSearch("experiment/.*", ".*", "sea ice", k=5):
print(hit.blob_name, hit.score)For files in a Context FS mount, pass "[0-9]+" as the blob pattern to search the page blobs and skip each file's attribute blob.
Search by time
TemporalSearch returns blobs whose last-modified timestamp lies in the inclusive range [time_begin, time_end], sorted ascending; 0 for either bound means unbounded.
for r in client.TemporalSearch("experiment/.*", ".*", time_begin=0, time_end=0, max_entries=20):
print(r.blob_name, r.last_modified)The docstrings describe last_modified as epoch nanoseconds, but the core stamps it from GetCurrentTimeNs(), which reads std::chrono::steady_clock. A bound computed from time.time() matches nothing. Treat last_modified as an ordering key within one run of the runtime. This is a suspected bug in the source, documented here as it behaves.
Move a blob between tiers
A blob's score decides its tier: 1.0 is the fastest, 0.0 the slowest. ReorganizeBlob gives the blob a new score and moves it to the tier that score selects. Evict frees the lowest-scored blobs off a tier, down to a minimum tier score and a byte count. With the default organizer: none these two calls are the only things that move data after a put.
tag.ReorganizeBlob("note0", 0.2f); // demote
float s = tag.GetBlobScore("note0"); // 0.2
auto ev = client->AsyncEvict(/*min_tier_score=*/0.9f, /*bytes=*/64 << 20);
ev.Wait();tag.ReorganizeBlob("note0", 0.0) # slowest tier
ok = client.ReorganizeBlob(tag.GetTagId(), "note1", 1.0) # True on successWhich tiers a score can reach depends on the tiers' own scores and the placement engine. The placement page explains how max_bw chooses.
Run operations asynchronously
Every Client method is asynchronous and returns a clio::run::Future over the task. Submit several, then Wait() on each and read the task's fields. The Tag wrapper adds AsyncPutBlob and AsyncGetBlob from private buffers. The buffer must stay valid until Wait() returns.
std::vector<std::string> bufs(4, std::string(4096, '\0'));
std::vector<clio::run::Future<clio::cte::core::GetBlobTask>> futs;
for (int i = 0; i < 4; ++i) {
futs.push_back(tag.AsyncGetBlob("page" + std::to_string(i), bufs[i].data(), bufs[i].size()));
}
for (auto &f : futs) f.Wait(); // served from the shared-memory cache: the future is empty, do not dereference itIn Python every Async* method returns a Future with wait(max_sec=-1), done() and result(max_sec=-1). The value of result() depends on the call: bytes for a get, a list for a query, a dict for the conditional puts, None for status-only operations.
tid = tag.GetTagId()
puts = [client.AsyncPutBlob(tid, f"page{i}", bytes(4096), 0) for i in range(4)]
assert all(f.wait() == 0 for f in puts)
gets = [client.AsyncGetBlob(tid, f"page{i}", 4096, 0) for i in range(4)]
pages = [f.result() for f in gets]Deferred puts
A deferred put stages a copy of the data in shared memory and returns a submission code immediately, without a future. The put is tracked in a process-wide registry. When shared memory is exhausted the next submission waits for the oldest puts to finish; max_inflight_bytes adds an explicit ceiling. Drain with AwaitPutsUntilSpace(0) and check DeferErrorCount() for puts that failed after submission. AsyncGetBlobDefer completes any pending deferred put for the blob before reading it.
for i in range(1000):
rc = client.AsyncPutBlobDefer(tid, f"rec{i}", record(i), 0, max_inflight_bytes=256 << 20)
assert rc == 0
client.AwaitPutsUntilSpace(0)
assert client.DeferErrorCount() == 0Conditional puts
Two Context flags make a put conditional. kPutIfAbsent creates the blob only if it does not exist and fails with kPutExistsRc otherwise. kPutIfVersion applies the put only when the blob is still at the version the caller read, and fails with a version-mismatch code otherwise. Python exposes them as AsyncPutBlobIfAbsent and AsyncPutBlobIfVersion, whose result() is a dict with code and version, and AsyncGetBlobVersioned returns code, version and data. Conditional puts need a non-empty payload, and a put overwrites a range without truncating, so use fixed-size records.
Delete
auto del = client->AsyncDelBlob(tag.GetTagId(), "note0");
del.Wait();
int rc = del->return_code_; // 0 on successassert client.DelBlob(tag.GetTagId(), "note0")Inspect targets
From Python, AsyncListTargets() returns the registered target names on this node, AsyncGetTargetInfo(name) returns a TargetInfo with the target's score, remaining space and byte and operation counters, and AsyncGetCapacity() returns total and remaining bytes. All three default to PoolQuery.Local(); Broadcast() sums or concatenates across nodes. A failed query raises RuntimeError from result().
for name in client.AsyncListTargets().result():
info = client.AsyncGetTargetInfo(name).result()
print(name, info.target_score, info.remaining_space)Where to go next
- API reference: every signature and default, and the compose YAML keys.
- Python module reference: each
clio_cte_core_extcall with its return type. - CTE core module: durability, capacity and startup behavior.
- Query files from Python: keyword search over a mount.