Skip to main content

Client

Struct Client 

Source
pub struct Client { /* private fields */ }
Expand description

A connection to one YTsaurus cluster.

Implementations§

Source§

impl Client

Source

pub fn new(proxy: &str) -> Self

Connects to proxy, with no token.

proxy may be a bare host (cluster.example.com, assumed HTTPS) or carry a scheme (http://localhost:8000).

Source

pub fn with_token(proxy: &str, token: impl Into<String>) -> Self

Connects to proxy using token for authentication.

Source

pub fn from_env() -> Result<Self>

Connects using YT_PROXY, and whatever token the environment offers.

The token is looked for the way the yt CLI looks for it, and stops at the first that has one:

  1. YT_TOKEN;
  2. the file named by YT_TOKEN_PATH;
  3. ~/.yt/token.

So a machine where the CLI already works needs no extra setup. A token read from a file is trimmed: one written with echo ends in a newline, and sending that produces an authentication failure that says nothing about a newline. An unreadable file is treated as no token rather than as an error, because that is what it means on a cluster that wants none.

§What else it reads

Everything a cluster can differ in that a caller cannot reach from here. Every example in this repository builds its client with this one method, so a policy settable only in Rust is a policy an example cannot be run under — which is how an installation that publishes its heavy proxies in another domain came to be unrunnable by any configuration at all, and had to be answered with a patch. Each of these is inert when unset, so a client built on a machine that sets none behaves exactly as Client::new does.

VariableEffect
YT_PROXY_SUFFIXCompletes a bare cluster name: YT_PROXY=hume with YT_PROXY_SUFFIX=.yt.example.net addresses hume.yt.example.net. Off unless set, and applied only to a name with no dot, no colon and no localhost in it — the gate the Go SDK uses. There is no builder for this one: in Rust, spell the address out.
YT_CA_BUNDLEA PEM file of roots, for a cluster behind a private CA. Read by the transport rather than here, and by Client::new too.
YT_HEAVY_PROXY_DOMAINSOne more domain — or several, comma- or space-separated — that /hosts may name a heavy proxy under. Client::with_heavy_proxies_under.
YT_HEAVY_PROXIES_ANYWHERE1, true or yes removes the domain rule outright. Client::with_heavy_proxies_anywhere.
YT_FILE_CACHEWhere Client::upload_worker_cached keeps its files, for an installation whose shared cache is read-only. Client::with_file_cache.

YT_HEAVY_PROXIES_ANYWHERE is applied after YT_HEAVY_PROXY_DOMAINS, so a machine that sets both is one where the rule is off — the wider of the two wins, rather than the order they happen to be exported in.

The environment can widen the heavy-proxy rule and cannot narrow it, which is deliberate: Client::with_heavy_proxies_in is the one mode that is a boundary rather than a heuristic, and a boundary that a variable could set is a boundary that a variable could move. Write that one in Rust.

A variable set to nothing counts as unset, all of them alike: export YT_FILE_CACHE= in a shell profile is how a knob gets turned back off, and reading it literally would point the cache at "". YT_PROXY included — an empty one earns the same message as a missing one, which is the message that says what to export.

§Errors

Returns ClientError::Config if YT_PROXY is not set, or set to nothing.

Source

pub fn with_poll_interval(self, interval: Duration) -> Self

Overrides how often Client::wait_for_operation polls.

Source

pub fn with_timeout(self, timeout: Duration) -> Self

Overrides the request timeout, which defaults to two minutes.

For a buffered command the limit is end to end, redirects included: an attempt takes its deadline once and the hops it makes share what is left of it, so a proxy that redirects cannot multiply the limit by the length of the chain. A retry is a fresh attempt and gets a fresh budget, which is what Client::with_retries bounds.

A streaming transfer — Client::read_table_streaming, Client::write_table_rows and their kin — is not cut off mid-table: there the timeout bounds each wait around the data (connecting, sending the request, the response headers), and the data itself moves for as long as it takes.

Source

pub fn with_retries(self, policy: RetryPolicy) -> Self

Overrides how a failed request is repeated.

The default is five attempts with a doubling delay, which covers the transient failures a shared cluster produces — a restarting proxy, a scheduler that has lost the master. RetryPolicy::none turns it off.

This applies to light commands only. Heavy ones — table and file I/O — are sent once whatever the policy says, because the documentation is explicit that they cannot be retried; a transaction is the way to make one atomic.

Source

pub fn with_file_cache(self, path: impl Into<String>) -> Self

Overrides where Client::upload_worker_cached keeps its files.

Defaults to the path the Python wrapper uses, so the cache is shared with whatever else the installation runs — and whatever expiry its administrators have set applies here too.

That default is read-only for an ordinary user on a managed installation, which the client itself handles — a refused cache degrades to a plain upload and says so — but which anything that needs to clear an entry cannot. YT_FILE_CACHE sets the same thing for a client built by Client::from_env.

Source

pub fn with_proxy_discovery(self, enabled: bool) -> Self

Overrides whether heavy commands ask the cluster where to go.

They do by default, which is what makes an upload work on an installation that separates proxy roles — unless the address this client was given is on loopback, where the lookup can only cost a round trip or name a host this process cannot reach. See the module documentation.

Both overrides have a use:

  • true for a cluster reached at localhost that really does have heavy proxies this process can reach — a port-forward into a real installation, where the discovered addresses resolve;
  • false to pin every command to the address given, which is what a balancer that already routes by role wants, and what to reach for if the lookup itself is the thing misbehaving.

This does not disturb what a client it was cloned from has already resolved.

Source

pub fn with_heavy_proxies_anywhere(self, enabled: bool) -> Self

Lets /hosts name a heavy proxy outside the configured address’s own domain.

Off by default. A discovered name is used only if it is the configured host itself or sits under that host’s parent domain — https://cluster.example.net will follow n0132-sas.example.net and will not follow n0132-sas.somewhere-else.net. A configured name with no dots in it, which is how YT_PROXY is usually written, is matched as a label instead: hume follows n0008-sas.hume.yt.example.net. A name that is refused is passed over; a /hosts answer that is refused entirely leaves the upload going to the configured address, which is where it went before this client routed anything, and the client says so once rather than leaving it to be deduced.

What that rule is worth, since it was once written down here as more than it is: it guards against a typo in a configuration and against an obviously foreign name. It is not what keeps a token where you put it. Steering a heavy command with a /hosts body means controlling that body — over https:// that is owning the proxy, which has the token already, and over http:// that is being a man-in-the-middle, who reads the token out of every light command without coming near this. Where the rule does bite is a proxy registering itself in the coordinator under an unintended name, and even there a shared parent domain on a hosting platform is shared with every tenant of it. Use Client::with_heavy_proxies_in where a real boundary is wanted.

Turn it on for an installation whose /hosts genuinely names another domain — a cluster fronted by a vanity address, or one whose data proxies live under a separate zone. Nothing else in the client changes; the scheme still comes from the configured address, a name carrying ://, /, @ or whitespace is still refused, and the configured port still carries through.

The symptom of needing it is an upload that reaches the configured address and is refused there — Control proxy may not serve heavy requests with input data — while Client::heavy_proxy shows a perfectly good address the client declined to use. The client says so itself, once, when it declines a whole /hosts answer, and the refusal it then collects carries the same sentence.

use ytsaurus_client::Client;

let client = Client::new("https://cluster.example.net")
    .with_heavy_proxies_anywhere(true);

This is all or nothing, which is why Client::with_heavy_proxies_under and Client::with_heavy_proxies_in exist beside it: a domain rule that misses by one label should not have to be answered by removing the rule — name the other domain, or the proxies themselves. The last of the three called is the one that decides.

This does not disturb what a client it was cloned from has already resolved.

Source

pub fn with_heavy_proxies_in<I, S>(self, names: I) -> Self
where I: IntoIterator<Item = S>, S: Into<String>,

Restricts heavy commands to a list of proxies written out by hand.

The third answer to “which of the names /hosts gives may this client send a token to”, and the only one that is a boundary rather than a heuristic. The domain rule is a guard against a typo and against an obviously foreign name — it cannot be more than that without a public-suffix list, and on a shared platform a shared parent domain means very little: yt-1234.us-east-1.elb.amazonaws.com and every other load balancer in that region share one. A list somebody wrote on purpose does not have that problem.

Names are compared without their ports and without case; the port a command is sent to still comes from the configured address, or from the /hosts entry when it carries one. Everything else in the client is unchanged: the scheme comes from the configured address, and a name carrying ://, /, @ or whitespace is still not a name.

use ytsaurus_client::Client;

let client = Client::new("https://cluster.example.net")
    .with_heavy_proxies_in(["n0132-sas.example.net", "n0133-sas.example.net"]);

An empty list admits nothing, so every heavy command stays on the configured address — Client::with_proxy_discovery is the plainer way to say that. The last of this and Client::with_heavy_proxies_anywhere to be called is the one that decides, and neither disturbs what a client this was cloned from has already resolved.

Source

pub fn with_heavy_proxies_under<I, S>(self, domains: I) -> Self
where I: IntoIterator<Item = S>, S: Into<String>,

Lets /hosts name a heavy proxy under a domain given here, as well as under the configured address’s own.

The middle setting, and on a large installation the only one that fits. A cluster addressed as cluster.example.net may publish its heavy proxies as n0132-sas.rack7.proxy-zone.net — a different domain, so the default rule refuses every one of them and no upload can leave the control proxy: Control proxy may not serve heavy requests with input data. The two answers that existed for that were writing all seventy-nine names out by hand, which goes stale the moment a proxy rotates, and Client::with_heavy_proxies_anywhere, which removes the rule. What such an installation actually has is one more domain.

use ytsaurus_client::Client;

let client = Client::new("https://cluster.example.net")
    .with_heavy_proxies_under(["proxy-zone.net"]);

A domain is matched as a suffix and as itself, without case: the entry above admits proxy-zone.net and anything under it, and nothing else. Every way a person writes one is accepted — surrounding space, a leading or trailing dot, a leading *, a scheme, a port — so a value read out of a configuration file works as written. An entry left with no dot in it is dropped rather than honoured: net would admit every .net host the cluster could name, which is Client::with_heavy_proxies_anywhere by accident.

The configured address’s own domain still applies — this widens the rule, it does not replace it — and an empty list therefore means exactly the default. A second call replaces the first, like every other setter here; it does not accumulate. And note the shape of the family rather than the reading of one word: with_heavy_proxies_anywhere(false) after this means the default rule and so discards these domains, which is not “stop widening”.

It is still a suffix rule, so it is worth what the domain rule is worth: a guard against a typo and against an obviously foreign name, not a boundary that holds a credential — see Client::with_heavy_proxies_anywhere for why that is, and Client::with_heavy_proxies_in for the version that is a boundary. A domain somebody wrote on purpose is a narrower statement than removing the rule, and it survives proxy rotation, which is the whole of what it claims.

The last of this, Client::with_heavy_proxies_anywhere and Client::with_heavy_proxies_in to be called is the one that decides, and none of them disturbs what a client this was cloned from has already resolved.

Source

pub fn with_hosts_timeout(self, timeout: Duration) -> Self

Overrides the budget for the /hosts lookup, which defaults to 800 ms.

The lookup sits in front of the first heavy command and gets its own budget rather than the client’s, because not getting an answer costs nothing worse than the routing this crate had none of a release ago — see Client::with_timeout for the one that bounds a command.

Raising it is the point. The budget used to be the smaller of 800 ms and the client’s own timeout, so it could only ever be lowered: a cluster that answers /hosts in 900 ms could not be routed to by any configuration at all. And 800 ms is not always generous — the first heavy command is often a client’s first request, which puts DNS, TCP and a TLS handshake inside the same budget.

use std::time::Duration;
use ytsaurus_client::Client;

let client = Client::new("https://cluster.example.net")
    .with_hosts_timeout(Duration::from_secs(3));
Source

pub fn with_hosts_retry_after(self, after: Duration) -> Self

Overrides how long routing stays off after it falls back, which defaults to ten seconds.

Two things end up here: a /hosts lookup that failed for a reason that might pass, and a pool whose every host has been dropped. Both mean “use the address the caller gave, and ask the cluster again in a moment”; this is the moment. A lookup that settled — no such endpoint, an answer that is not a list of names, a cluster that names no heavy proxy — runs on the other clock instead: it is asked about again one Client::with_host_list_refresh_interval later, like any other answer that has grown old. So does a failed refresh, deliberately — a pool in hand still routes, so nothing there is urgent enough for this window.

Shorter brings routing back sooner after a cluster recovers, and costs a lookup more often while it is broken. Longer is the other trade.

Source

pub fn with_host_list_refresh_interval(self, interval: Duration) -> Self

Overrides how old a /hosts answer may grow before a heavy command re-asks, which defaults to one minute.

The default is the documentation’s own advice — “a good strategy is to re-query the /hosts list every minute or every few queries” — and the refresh is lazy, the way the C++ SDK does it: the heavy command that finds the list stale asks first, and a client that stops uploading stops asking. There is no background thread. A refresh that fails keeps the previous answer in use rather than dropping routing on the floor, and waits out another interval before asking again.

The refresh is also what restores a proxy the client dropped: a heavy command that fails for a reason attributable to the host it went to — a refused connection, a 503, a certificate that does not match that host’s name — takes that host out of the pool, and the next fresh answer that still names it puts it back.

use std::time::Duration;
use ytsaurus_client::Client;

let client = Client::new("https://cluster.example.net")
    .with_host_list_refresh_interval(Duration::from_secs(300));

Shorter follows the cluster’s load-ordering more closely and costs a lookup more often — Duration::ZERO re-asks before every heavy command. Duration::MAX disables the refresh: the first answer is then kept as long as it keeps working, though a failed host is still dropped and an emptied pool still falls back and re-asks.

Source

pub fn with_job_diagnostics(self, enabled: bool) -> Self

Turns the failed-job report in Client::wait_for_operation on or off.

On by default: when an operation fails, the client asks the cluster which jobs failed and what they printed, and puts that in the error. That costs one list_jobs and a few get_job_stderr calls per failed operation. The YTsaurus documentation asks that list_jobs not be used without an administrator’s approval, so this is the way to switch it off on an installation where that approval was not given.

Source

pub fn with_transaction(self, id: impl Into<String>) -> Self

Binds this client to an existing transaction.

Every command it then sends happens inside that transaction. This is the low-level door: Client::start_transaction is the one that starts a transaction, keeps it alive and aborts it if the work does not finish, and Client::attach_transaction is the one that turns an id from elsewhere into such a handle — pinging, able to commit and abort.

This binding does neither: nothing pings the transaction on this path, so it expires on the cluster’s schedule unless its owner — or Client::ping_transaction — is pinging it, and finishing it takes Client::commit_transaction or Client::abort_transaction with the id. What it buys over attach_transaction is costlessness: no round trip, no thread.

Source

pub fn transaction_id(&self) -> Option<&str>

The transaction this client is bound to, if any.

Source

pub fn with_trace_context(self, context: &TraceContext) -> Self

Puts every request this client sends into context’s trace.

The cluster traces itself: the proxy opens a span for each request, and a request that names a trace has its span put inside that one instead of starting an orphan. So this is the cheap half of making a launch visible — nothing is emitted from this process, and the work the cluster does on its behalf turns up under the caller’s own trace.

use ytsaurus_client::{Client, TraceContext};

// A service passing on the trace it was called in.
let incoming = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
let client = Client::new("http://localhost:8000")
    .with_trace_context(&TraceContext::parse(incoming)?);

TraceContext::new starts a trace for a program that was not called by anything, and TraceContext::yt_trace_id spells its id the way the cluster’s own logs and UI do.

A Transaction started from this client inherits the context, pings included — the transaction is part of the same piece of work, and a commit that hung is one of the things a trace is for.

Source

pub fn traceparent(&self) -> Option<&str>

The traceparent header this client sends, if it was given one.

Source

pub fn tracestate(&self) -> Option<&str>

The tracestate header this client sends, if the context it joined carried one. See TraceContext::with_tracestate.

Source

pub fn start_transaction(&self) -> Result<Transaction>

Starts a transaction, and keeps it alive while the handle lives.

Everything sent through the returned Transaction is invisible to everything else until it commits, and is discarded if it does not:

let tx = client.start_transaction()?;

tx.create("table", "//tmp/out")?;   // no one else can see it yet
tx.write_table("//tmp/out", &rows)?;

tx.commit()?;                       // and now everyone can

The transaction lasts 30 seconds without a ping — the cluster’s own default — and the handle pings it every ten, so an operation that runs for an hour is fine. Client::start_transaction_with changes the timeout.

§Errors

Returns ClientError if the transaction cannot be started.

Source

pub fn start_transaction_with(&self, timeout: Duration) -> Result<Transaction>

Starts a transaction that expires timeout after its last ping.

The handle pings three times per timeout, so this is about what happens when the handle is gone: how long the transaction holds its locks after the process holding it dies without aborting. Shorter frees them sooner; longer survives a longer pause.

§Errors

Returns ClientError if the transaction cannot be started.

Source

pub fn attach_transaction(&self, id: &str) -> Result<Transaction>

Attaches to a transaction something else started, and keeps it alive.

The receiving half of Transaction::detach: one process starts a transaction and detaches, hands the id over, and this turns the id back into a real Transaction — a bound client, a pinging thread, and commit/abort/ping that work. Two things differ from a handle the same process started, and both follow from not being the owner:

  • Dropping it detaches rather than aborts — the pings stop and nothing is sent. The C++ client’s destructor draws the same line, and for the same reason: an attacher’s ? must not destroy work the process that started the transaction is still counting on. An explicit Transaction::abort still aborts; only the drop differs.
  • The ping interval is read, not chosen. Pinging needs the transaction’s timeout and the id alone does not carry it, so this asks the cluster for #<id>/@timeout — one round trip, which is also what makes attaching to a transaction that is gone fail here, rather than on the first command sent through the handle.

It pings before it returns, one more round trip. @timeout is the configured lifetime and says nothing about how much of it is left: the id carries no hint of when its last holder pinged, so a handoff that took longer than two thirds of the timeout would otherwise hand back a handle whose first ping is already too late. That ping restarts the cluster’s clock at the attach, and doubles as the liveness probe this call reports on.

So this is two retryable round trips, both on this client and so under its retry policy — five attempts of two minutes by default, backoff between — where the keep-alive’s own pings run one attempt on a budget of half the ping interval. A ping the caller is waiting on should not fail over one dropped packet; a keep-alive ping is retried by being sent again next interval.

Nothing stops two attaches to the same id. Each is a real handle with a thread of its own, and they simply ping the same transaction twice as often; whichever commits or aborts first decides it, and the other’s next command fails with No such transaction. There is no registry, on purpose — a second process attaching is the whole point, and this process is not in a position to know about it.

The handle always pings. One that did not would be Client::with_transaction — the plain binding, which already exists — plus Client::ping_transaction, Client::commit_transaction and Client::abort_transaction, which take the bare id; reach for those where a thread per transaction is not wanted. (The Go SDK spells that choice AttachTx(id, &AttachTxOptions{AutoPingable: false}).)

let tx = client.attach_transaction(&id_from_elsewhere)?;

tx.create("table", "//tmp/out")?;   // inside the shared transaction
tx.commit()?;                       // and now published, by this process
§Errors

Returns ClientError if the transaction does not exist or the timeout cannot be read. The error names the id and the operation itself, because the cluster’s own answer does not always do either. Both spellings were observed on a local cluster: an expired id earns Error resolving path #<id>/@timeout around No such object <id> — object, not transaction, since the id is addressed as one — while an id that never named anything is refused as Unknown cell tag 0, with no id in it at all. A transaction that expires between the two round trips fails the same way, on the ping: No such transaction.

Source

pub fn ping_transaction(&self, id: &str) -> Result<()>

Tells the cluster a transaction is still wanted, by bare id.

A held Transaction does this on its own thread; this is for a process that has nothing but the id — between a Transaction::detach in one process and the commit in another, somebody must say the transaction is still wanted, or it expires its timeout after its last ping (30 seconds by default; verified on a local cluster with a two-second timeout left alone for four). A ping is also the cheapest liveness probe: the cluster answers one for a transaction that is gone with No such transaction.

§Errors

Returns ClientError if the transaction has expired, was aborted, or never existed.

Source

pub fn commit_transaction(&self, id: &str) -> Result<()>

Publishes everything done in a transaction, by bare id.

What lets a process finish a transaction it did not start — the other end of a Transaction::detach, without the round trip and the ping thread of Client::attach_transaction.

Sent under a mutation ID, because a commit is not idempotent: the second commit of the same transaction is refused with No such transaction, which reads like the first one failed. The mutation ID makes a retried commit the same commit rather than a second one.

§Errors

Returns ClientError if the commit fails — including No such transaction for one that expired, was aborted, or was already committed.

Source

pub fn abort_transaction(&self, id: &str) -> Result<()>

Discards everything done in a transaction, by bare id.

Forgiving, unlike Client::abort_operation: aborting a transaction that already committed, aborted or expired — or one that never existed — answers {}, verified on a local cluster. So this is safe to send on any cleanup path, and it is retried freely on the same grounds.

§Errors

Returns ClientError if the request fails. The transaction expires on its own either way, once nothing is pinging it.

Source

pub fn heavy_proxy(&self) -> Result<Option<String>>

Asks the cluster for the least-loaded heavy proxy, if it has one.

The client already does this for itself. Heavy commands — table and file data, in either direction — resolve a heavy proxy on their own and go there; see the module documentation for when, and for how long the answer is kept. So this is no longer the way to make an upload work: it is the way to see the address, or to hand it to something that is not this client — a second Client, another process, a curl.

It asks every time and shares nothing with what the client resolved for itself, so calling it neither costs nor changes anything the next command does. It also reports the name as the cluster gave it, before the checks automatic routing puts it through — which is what makes it the way to see why a host was declined. A name here that the uploads are not using is the symptom Client::with_heavy_proxies_anywhere exists for.

It shares the lookup’s budget, though: one attempt bounded by Client::with_hosts_timeout — 800 ms unless that says otherwise — rather than the client’s retry policy and request timeout. The budget belongs to the question, not to whoever asked it.

§Errors

Returns ClientError if the request fails, or if /hosts does not answer with the documented list of host names. Ok(None) means the cluster answered and named no heavy proxy — which a failure must not be allowed to look like, since the caller’s next move is to stop looking.

Source

pub fn exists(&self, path: &str) -> Result<bool>

Whether a Cypress node exists.

§Errors

Returns ClientError if the request fails.

Source

pub fn create(&self, node_type: &str, path: &str) -> Result<()>

Creates a Cypress node, e.g. table, file or map_node.

Creates missing parents and succeeds if the node already exists.

§Errors

Returns ClientError if the request fails.

Source

pub fn create_table(&self, path: &str, schema: &TableSchema) -> Result<()>

Creates a table with a schema.

A schematised table is checked on every write, stores its columns in their own types, and can be sorted and merged; an unschematised one takes anything and finds out later.

let schema = TableSchema::new([
    Column::new("host", ColumnType::Utf8).required().key(),
    Column::new("size", ColumnType::Int64).required(),
]);
client.create_table("//tmp/visits", &schema)?;

Unlike Client::create, this fails if the path already exists. That is deliberate: the cluster ignores the attributes of a create it skips, so an ignore_existing version of this would quietly leave the old table with the old schema and report success. Changing the schema of a table that exists is alter_table’s job.

§Errors

Returns ClientError::Config if the schema is one the cluster would refuse, or ClientError if the request fails.

Source

pub fn alter_table(&self, path: &str, schema: &TableSchema) -> Result<()>

Changes the schema of a table that already exists.

The other half of Client::create_table: a table outlives the program that made it, and the rows it holds gain columns.

let wider = TableSchema::new([
    Column::new("host", ColumnType::Utf8).required().key(),
    Column::new("size", ColumnType::Int64).required(),
    Column::new("referrer", ColumnType::Utf8), // new, and optional
]);
client.alter_table("//tmp/visits", &wider)?;

A table with rows in it accepts only changes that ask less of the rows already written. Watched on a cluster, on a table holding two rows — and each refusal says which column and why:

Change
add an optional column, anywhere in the orderallowed
make a required column optionalallowed
strict → non-strictallowed
add a required columnCannot insert a new required column "must" into a non-empty table
remove a columnCannot remove column "size" from a strict schema
change a column’s typeType … is modified in non backward compatible manner
rename a columnread as a removal, and refused as one
make the table sortedCannot change schema from unsorted to sorted
non-strict → strictChanging "strict" from "false" to "true" is not allowed

Two consequences worth knowing before either becomes permanent:

  • An empty table accepts all of it — dropping columns, changing types, becoming sorted. So a schema change tried out on an empty table proves nothing about the same change on a full one.
  • A non-strict schema can never gain a named column: Cannot insert a new column "note" into non-strict schema. Relaxing strict is a one-way door out of schema evolution.

Unlike create, the schema here is a top-level parameter rather than an attribute — the two commands are exact opposites on this, and create silently ignores the spelling alter_table requires.

§Errors

Returns ClientError::Config if the schema is one the cluster would refuse outright, or ClientError if the change is rejected as incompatible.

Source

pub fn table_schema(&self, path: &str) -> Result<YsonValue>

The schema of a table, as the cluster stores it.

Returns the raw YSON: the cluster answers with more than it was given — every column carries required, type and type_v3 whichever was written, and the keys come back in alphabetical order.

§Errors

Returns ClientError if the request fails.

Source

pub fn remove(&self, path: &str) -> Result<()>

Removes a Cypress node.

The node must exist, and a map node must be empty — the cluster’s own defaults, and the safe ones: a mistyped path fails instead of deleting whatever it happened to name. Client::remove_tree is the deliberate spelling for a subtree.

§Errors

Returns ClientError if the node does not exist, is a non-empty map node, or the request fails.

Source

pub fn remove_tree(&self, path: &str) -> Result<()>

Removes a Cypress node and everything under it. Succeeds if it is already absent.

This is recursive plus force: the spelling for “make this path not exist”, whatever is there now — which is also why it deserves a moment of care with the argument.

§Errors

Returns ClientError if the request fails.

Source

pub fn list(&self, path: &str) -> Result<Vec<String>>

The names of a node’s children.

Not sorted. The order is the cluster’s own and has no meaning; a listing of three dated tables came back as the second, the third and then the first. Sort it if the order matters.

A path that is not a map node is an error rather than an empty list — "List" method is not supported — and so is a path that does not exist.

§Errors

Returns ClientError if the request fails, or if the cluster marks the answer incomplete: a listing that is silently short is worse than no listing.

Source

pub fn copy(&self, source: &str, destination: &str) -> Result<()>

Copies a node, creating missing parents.

Fails if destination exists; Client::copy_replacing is the one that overwrites.

§Errors

Returns ClientError if the request fails.

Source

pub fn copy_replacing(&self, source: &str, destination: &str) -> Result<()>

Copies a node over whatever is at destination.

§Errors

Returns ClientError if the request fails.

Source

pub fn move_node(&self, source: &str, destination: &str) -> Result<()>

Moves a node, creating missing parents.

Fails if destination exists; Client::move_replacing is the one that overwrites, and the pair is how a result is published: write a staging table, then move it over the live one.

Named move_node because move is a Rust keyword, and client.r#move at every call site would be a worse tax than the four extra characters.

§Errors

Returns ClientError if the request fails.

Source

pub fn move_replacing(&self, source: &str, destination: &str) -> Result<()>

Moves a node over whatever is at destination.

§Errors

Returns ClientError if the request fails.

Creates a link at link_path pointing at target.

A link resolves to its target, so //tmp/latest/@row_count reads the target’s row count. To ask about the link itself, put & after its path: //tmp/latest&/@target_path. Without the & the question goes through to the target and is answered as if the link were not there.

Fails if link_path exists; Client::link_replacing is what points an existing link somewhere else.

§Errors

Returns ClientError if the request fails.

Points a link at target, replacing whatever is at link_path.

The //tmp/thing/latest pattern: publish under a dated name, then move the link. Readers that follow the link see the old version until this call and the new one after it, and never a half-written table.

§Errors

Returns ClientError if the request fails.

Source

pub fn lock(&self, path: &str, mode: LockMode) -> Result<Lock>

Takes a lock, or fails because somebody else holds one.

Only inside a transaction: a lock lives as long as the transaction that took it, and there is nothing else for it to belong to. A client that is not in one is told so here rather than by the cluster.

The failure is worth reading — it names the transaction that won:

Cannot take "exclusive" lock for node //tmp/live since "exclusive" lock
is taken by concurrent transaction 4-dac2-10001-eb1b

Client::lock_waiting queues for it instead of failing.

§Errors

Returns ClientError::Config if this client is not in a transaction, or ClientError if the lock is refused.

Source

pub fn lock_waiting( &self, path: &str, mode: LockMode, wait_for: Duration, ) -> Result<Lock>

Queues for a lock, and waits until it is held.

A waitable lock is granted later, or never — the cluster answers immediately with a lock that is pending, and it becomes acquired when the transactions ahead of it end. Returning that lock as though it were held is the mistake this command exists to make impossible: this polls until the cluster says acquired, and gives up after wait_for.

The deadline is not a nicety. A request can queue for something that will never happen and the cluster will not say so: a transaction that already holds a snapshot lock on the node is refused an exclusive one outright, but the waitable version of the same request is queued behind a lock only that transaction’s own end will release.

§Errors

Returns ClientError::Config if this client is not in a transaction or the wait ran out, or ClientError if a request fails. A lock that is still queued when the wait runs out stays queued until the transaction ends.

Source

pub fn get(&self, path: &str) -> Result<YsonValue>

Reads a node attribute, such as @row_count.

§Errors

Returns ClientError if the request fails.

Source

pub fn row_count(&self, path: &str) -> Result<i64>

Number of rows in a table.

§Errors

Returns ClientError if the request fails or the attribute is absent.

Source

pub fn execute_batch( &self, batch: &BatchRequest, ) -> Result<Vec<Result<YsonValue>>>

Executes every part of a BatchRequest in one round trip, and answers with a Result per part.

The parts fail individually — that is the entire point of the shape. One part hitting a node that already exists does not cost the other eleven their tables, and collapsing the answers into one Result would lose exactly the thing batching makes harder to see. The outer Result is for the envelope alone: the request that could not be sent, the response that could not be read.

let mut batch = BatchRequest::new();
batch
    .create("map_node", "//tmp/pipeline")
    .create("table", "//tmp/pipeline/clicks")
    .exists("//tmp/elsewhere");

for part in client.execute_batch(&batch)? {
    match part {
        // The envelope is keyed by what each command returns —
        // `{node_id=…}` for a create, `{value=…}` for an exists.
        Ok(answer) => println!("{answer:?}"),
        Err(error) => eprintln!("{error}"),
    }
}

Each Ok carries the part’s own answer exactly as that command would have answered alone — {node_id=…}, {value=…}, {} for a set — and each Err is a ClientError::Cluster named after the part’s command, flattened outer-plus-innermost like every other cluster error here. Results come back in the order the parts went in; watched on a local cluster, where a batch of create·set·get·remove answered [error 501, ok, ok, error 500] in exactly that order. An answer with the wrong number of results, or a part result shaped like nothing this client knows, fails the whole call as ClientError::Decode rather than being read as somebody’s success.

§The wire

The command is execute_batchREGISTER_ALL(TExecuteBatchCommand, "execute_batch", Null, Structured, true, false) in the cluster’s own registry: volatile and light, so a POST. The parts travel as requests=[{command=…; parameters={…}; input=…}] and the answer is the v4 envelope {results=[{output=…}|{error=…}]} (command reference; TExecuteBatchCommand in etc_commands.cpp; both shapes confirmed against a local cluster).

The parameters go in the request body, not the X-YT-Parameters header that carries every other command’s. A batch’s parameters are the batched commands, and a header has a size nobody promises; the C++ client makes the same choice for this same command (THttpRawBatchRequest::ExecuteBatch sends the parameter node as the POST body), and the proxy reads body parameters for any POST and merges them with the header’s (TContext::CaptureParameters in context.cpp — query string, then header, then body). Measured here: requests in the body and mutation_id in the header land as one parameter set.

§Retries, and what makes them safe

A batch of the typed parts retries like any light command, and a mutating one retries under a mutation id — because the cluster spreads that id over the parts. The driver takes the batch’s own id and hands part k the id plus k (Options.GetOrGenerateMutationId() then NRpc::GenerateNextBatchMutationId per part in TExecuteBatchCommand::DoExecute; the increment is ++id.Parts32[0], yt/yt/core/rpc/helpers.cpp), stamping it and the batch’s retry flag into every volatile part. A replay of the whole batch therefore replays every part under its original id, and the master’s mutation cache answers each with its first response. Measured on a local cluster: a two-BatchRequest::create_table batch sent under an explicit id, then sent again with retry=%true, answered the same two node ids both times — where the same batch under a fresh id got two 501 already exists.

The measurement uses create_table and not BatchRequest::create on purpose, and repeating it with create proves nothing: create sends ignore_existing, so a second send answers with the old node’s id whether or not the cluster recognised a replay. Measured that way too — create under a fresh id returned the same two ids as the first send, with no mutation cache involved at all. create_table omits ignore_existing, so its second send fails unless it was deduplicated, which is what makes the identical ids mean something.

That safety is the master’s, which is why the default is per-part kind: parts this crate models are Cypress commands the master’s cache covers, so their batches go out Repeatable::WithMutationId (or Repeatable::Freely when every part is a read, since such a batch mutates nothing). A BatchRequest::raw part may name a command the cache does not cover — the scheduler commands are the measured example, where a replayed id turns a success into No such operation — so a batch carrying one is sent once, exactly as Client::raw_command is.

§Transactions

A client bound to a transaction puts the parts in it — each part is stamped with transaction_id, not the envelope. The envelope has no transaction to be in, and the distinction is measurable: an outer transaction_id was dropped in silence by a local cluster, the part’s create landing outside the transaction and surviving its abort. A part that already names a transaction keeps its own, and a part whose command takes none is left alone, both as the transport itself would have it.

§A big batch is several requests, and a failed one leaves a prefix

More parts than BatchRequest::with_max_part_size allows are split into consecutive execute_batch requests — the C++ client’s BatchPartMaxSize behaviour, defaults included — with the results stitched back in part order and a mutation id per request. There is no rollback across them: when a later request fails wholesale, the earlier ones have already run and their parts have taken effect, the same way the C++ client’s ExecuteBatch throws with the earlier requests applied.

What this method does not do is throw that prefix away. A split batch that stops part of the way through fails with ClientError::BatchInterrupted, which carries every answer already received, in part order, beside the failure that stopped it — so a caller can see which parts landed and pick up from answered.len(). Re-running the same BatchRequest is not how to recover: a second execution mints fresh mutation ids, so the parts that already applied are applied again rather than deduplicated. Keep a batch inside one request’s worth if that matters, or give the sequence a transaction.

answered is what came back, which is not the same as what was applied, and the difference is the whole failed request. A request refused while executing has no per-part results and has nonetheless run every one of its parts — the driver collects the sub-requests into callbacks, runs them all through CancelableRunWithBoundedConcurrency, and then throws away the entire result list at .ValueOrThrow() the moment one entry is a throw. Dispatch is never aborted, so this is not a race and there is no way to arrange the parts to limit it: measured on a local cluster, a create beside a part naming an unknown command created its node with the bad part first and last, two creates around one both landed, and at concurrency=1 eight creates followed by the bad part all eight landed — every time answered Unknown command … with no results at all.

The bound worth knowing is the other one: a request refused while its parameters are being read runs nothing. Validation failed at /concurrency, Error loading parameter /requests and Missing required parameter /requests all left a create in the same request with no node behind it. Parse-time failure means none of it ran; execution-time failure means all of it did.

So the parts before answered.len() are settled, and the request that failed is unknown territory — not because some of it might have run, but because all of it did and none of it said what happened. That is what a transaction is for.

§A redirect this batch cannot follow

The parts travel in the body, so this is the crate’s first light command with bytes in one — and the redirect rule reads a body as data a redirect must not hand to another origin (RedirectRefusal::Payload). A cross-origin 3xx on a batch is therefore refused where the same creates sent one at a time are bodiless POSTs the rule deliberately lets through. It is narrow — a client with a token is refused a cross-origin hop anyway, by the credentials rule — but a tokenless client behind a balancer that canonicalises to another origin finds batching breaks what individual calls did. Address the origin the balancer canonicalises to, and the hop never happens.

§Errors

Returns ClientError::Config for an empty batch — the cluster would answer {results=[]} and this crate does not report a no-op as work done — ClientError::BatchInterrupted when a split batch stops after some of its requests have applied, and otherwise ClientError as any command fails. Per-part failures are not errors of this method: they are the Err halves of the vector.

Source

pub fn execute_batch_with( &self, batch: &BatchRequest, mutation_id: Option<&MutationId>, ) -> Result<Vec<Result<YsonValue>>>

As Client::execute_batch, with a caller-supplied MutationId.

The guarantee is the one Client::raw_command_with describes and the one a single process cannot give itself: persist the id, and a batch replayed after a crash is deduplicated against the send that already happened instead of applying every part a second time. Measured on a local cluster through this method: a batch of two BatchRequest::create_table parts sent under an explicit id, then sent again under id.as_retry(), answered the same two node ids both times — where the same batch under a fresh id got two 501 already exists.

Reach for create_table and not BatchRequest::create when checking this by hand. create sends ignore_existing, which makes a second send answer with the old node’s id on its own: measured, a two-create batch under a fresh id returned ids identical to the first send’s, which looks exactly like a deduplicated replay and is not one. create_table sends no ignore_existing, so identical ids there can only be the mutation cache.

That works because the cluster spreads the id over the parts rather than deduplicating the envelope: the driver hands part k the batch’s id plus k, so a replay replays each part under the id its first send used. It is also why an id covers one request and not a split batch — see the refusal below.

let id = MutationId::new();
// …persist `id.as_str()` here, before sending…
let made = match client.execute_batch_with(&batch, Some(&id)) {
    Ok(made) => made,
    // After a crash, the same id marked as a replay: the cluster
    // answers with what the first send did, whether or not it landed.
    Err(_) => client.execute_batch_with(&batch, Some(&id.as_retry()))?,
};

An id is stamped whatever the batch’s own retry class works out to, including on an all-read batch that would otherwise carry none — the two answer different questions, as Client::raw_command_with spells out. It does not make a send-once batch retriable in-process: a batch holding an unclassified BatchRequest::raw part is still sent once.

§Errors

As Client::execute_batch, and additionally ClientError::Config when an id is given for a batch that would be split into more than one request. One id cannot cover several: the driver derives each part’s id by incrementing the batch’s, so a second request under anything derived from the same id would collide with the first request’s parts and be answered with their results. Raise BatchRequest::with_max_part_size until the batch fits one request, or send it without an id.

Source

pub fn upload_worker(&self, local: impl AsRef<Path>, remote: &str) -> Result<()>

Uploads a local file to Cypress, marking it executable.

This is what makes a worker runnable on a node: without the executable attribute YTsaurus copies the binary but refuses to exec it, and the job fails with a permission error that does not mention the attribute.

§Errors

Returns ClientError if the file cannot be read or the upload fails.

Source

pub fn upload_current_exe(&self, remote: &str) -> Result<()>

Uploads the running executable to Cypress, marked executable.

This is the one-binary pattern: the same program launches the operation and runs as its job, telling the two apart with ytsaurus_job::is_inside_job. The binary on the cluster is then by construction the one you just built — the whole “I uploaded a stale worker” class of bug disappears.

The running executable has to be something a node can exec, so its ELF header is checked before the upload: Linux, x86-64, statically linked. Launching from macOS, or from a Linux host where the launcher is dynamically linked, it is not — this returns ClientError::NotAWorker naming the reason, instead of uploading a binary that fails on the node minutes later. Build the worker with scripts/build-worker.sh and upload it with Client::upload_worker in that case.

§Errors

Returns ClientError::NotAWorker if the running executable cannot run on a node, or ClientError if the upload fails.

Source

pub fn upload_worker_cached( &self, local: impl AsRef<Path>, ) -> Result<CachedFile>

Uploads a worker, or finds it already on the cluster.

Keyed by the file’s MD5, so an unchanged binary is uploaded once and every later launch reuses it. That is the difference between a dev loop that re-sends tens of megabytes on every run and one that does not.

The cached node is named after the hash, so the returned CachedFile::name is the name to give it in the sandbox — see MapSpec::with_local_file_named:

let worker = client.upload_worker_cached("target/.../my_job")?;
let spec = MapSpec::new("./my_job", ["//tmp/in"], ["//tmp/out"])
    .with_local_file_named(&worker.path, &worker.name);

The cache is shared: Client::with_file_cache defaults to the path the Python wrapper uses, so an installation that already expires old entries there expires these too.

§A cache you may not write to

On an installation where that shared path is maintained by its operators, an ordinary user may read it and nothing more — and the cluster answers a write with Access denied. That is a degraded cache, not a failed upload: the worker goes up outside the cache instead, to a path of its own under //tmp, and the launch proceeds.

It is warned about rather than passed over, on stderr — as a WARN event where the tracing feature is on — because the state is permanent until someone acts on it and invisible otherwise: every launch re-sends the whole binary, and every launch leaves a node behind that no cache expiry will collect. The warning names Client::with_file_cache, which is the one line that puts a cache back.

Only the cluster’s refusal of the cache is treated this way — creating the cache directory, creating the staging node inside it, and the handover to put_file_to_cache. Any other failure, including an Access denied on anything else, is returned.

CachedFile::cached is which of the two happened, and it is the field to read before doing anything to CachedFile::path: on the fallback path that node is this launch’s own and nobody else’s, while on the ordinary path it is the installation’s shared cache entry.

§Errors

Returns ClientError if the file cannot be read or the upload fails.

Source

pub fn file_from_cache(&self, md5: &str) -> Result<Option<String>>

Looks up a file in the cluster’s file cache by its MD5.

None means nothing is cached under that hash — including when the cache directory does not exist yet, which is what Client::upload_worker_cached creates on its way past, on a cluster that lets it.

A lookup and nothing more: it sends no mutation, so it works against a cache the caller may only read.

§Errors

Returns ClientError if the request fails.

Source

pub fn put_file_to_cache(&self, path: &str, md5: &str) -> Result<String>

Hands a file already written to Cypress to the file cache.

The cluster verifies that the node’s MD5 is the one given, which is why it must have been written with compute_md5. Returns the path the file now lives at.

§Errors

Returns ClientError if the request fails.

Source

pub fn write_file(&self, path: &str, contents: &[u8]) -> Result<()>

Writes raw bytes to a Cypress file, replacing its contents.

§Errors

Returns ClientError if the request fails.

Source

pub fn read_file(&self, path: &str) -> Result<Vec<u8>>

Reads a whole Cypress file into memory.

The mirror of Client::write_file, and the buffered half of the pair: for a worker binary fetched back, a config a launcher inspects — results, not bulk data. For a file that does not fit, Client::read_file_streaming moves the same bytes without holding them.

The whole file is held in memory, and there is a ceiling: 512 MiB. That is the transport’s cap on any buffered response, counted in the bytes that land in the Vec — and a file past it is refused rather than truncated, with a ClientError::ResponseTooLarge that names the number and names the streaming half. A file of exactly the ceiling is not past it. A worker binary is comfortably under; a dataset someone stored as a file may not be, and that is exactly the case the pair comes in two halves for.

512 MiB held is not 512 MiB of process. The buffer grows by doubling and copies as it grows, so both halves are resident for the length of a copy — up to about 1.5× the cap where the allocator cannot extend in place. Measured in a release build: a read that hands back 536 870 911 bytes peaks at 544 178 176 of resident set, and a 600 MiB read refused by the cap peaks at 611 385 344. Size for that, not for the ceiling.

The cap counts decoded bytes because the compressed ones are not the same quantity and are not close to it: this client asks for gzip, and measured against a cluster, a 600 MiB file of zeros crosses the wire in 611 522 bytes. A cap on what arrives would have let all 600 MiB into memory — which is what it did until this was fixed.

path is a plain node path//tmp/worker. Not a rich one, and the reason is worth spelling out, because a rich path here does not fail so much as quietly do nothing. Measured on a cluster, on a file of 1000 bytes:

  • <lower_limit={offset=0};upper_limit={offset=10}>//tmp/f reads back all 1000 bytes and passes the size check. A file is sliced by the command’s own offset and length parameters, not by limits on the path, so limits written there are accepted and ignored — and the caller who thought they had asked for ten bytes is told nothing. <append=%false>//tmp/f is the same story with a harmless attribute.
  • //tmp/f[#0:#10] also reads back all 1000 bytes, and then fails: the size check builds {path}/@uncompressed_data_size out of this string textually, and //tmp/f[#0:#10]/@uncompressed_data_size is not a path the cluster will parse — Error reading parameter /path: Unexpected token "/" of type "slash". A whole file downloaded and then refused over a range that was never going to be honoured.

So: a plain path. Selection on reads is #12, and belongs in parameters this method would have to grow, not smuggled in through this argument.

The body’s length is checked against the size Cypress records for the node. That is not pedantry — the proxy reports a mid-stream failure in a trailer this client cannot see (see TableReader for the trailer gap), and a file’s bytes carry no framing of their own: where a truncated table leaves a record that does not parse, a truncated file just ends, looking exactly like a shorter file. So after the read, one light get fetches the node’s @uncompressed_data_size — the byte count of the content, whatever compression the node’s own codec applies beneath it — and a body of any other length is an error rather than a file.

The two requests are not atomic, and the race runs both ways. A writer replacing the file between them can fail the check for a body that was complete when it was sent — the ordinary hazard of reading what someone else is rewriting, surfaced as an error rather than as a mix of the two versions. The converse is rarer and quieter: a body genuinely cut short at N bytes, racing a replacement whose own @uncompressed_data_size is exactly N, passes the check, and a truncated read of the old version is returned as a whole file. That one cannot be closed from here — the only in-band verdict on a cut stream is the proxy’s trailer, which ureq 3.3 does not read, so there is no header to prefer over the second request. A reader who needs a file pinned while others replace it takes a LockMode::Snapshot lock in a transaction, which is exactly what that mode is for, and closes both directions at once.

Verified against a local cluster: a 4 MB Client::write_file of non-UTF-8 bytes comes back byte-for-byte through both halves of the pair, an empty file reads back empty, and a node carrying compression_codec=zlib_6 — 1 000 000 logical bytes, 4 214 on disk — reads back its logical bytes with the check passing, which is the case that would break if the attribute were the on-disk size. And a 600 MiB file of zeros — 611 522 bytes on the wire — is refused rather than held, while read_file_streaming moves all 629 145 600 of it.

§Errors

Returns ClientError if the request fails, if the response is larger than the 512 MiB this holds in memory — a ClientError::ResponseTooLarge, which is never retried and never blamed on the proxy that served it — if the node’s size cannot be read — the check refuses loudly rather than quietly not happening — or if the body’s length is not the size the cluster records. A missing path fails the read itself, before the size is ever asked for: code 1, Error getting basic attributes of user objects, with the resolve error nested inside — a category outside and the reason within, as a missing table is reported too.

Source

pub fn read_file_streaming(&self, path: &str) -> Result<FileReader>

Reads a file as a stream, without holding it.

The same bytes Client::read_file returns, arriving as they come off the connection — and a file is exactly the thing that might not fit in memory, which is why Client::write_file’s mirror comes in two halves. What comes out is a plain Read:

let mut file = client.read_file_streaming("//tmp/worker")?;
std::io::copy(&mut file, &mut std::fs::File::create("worker")?)?;

Client::read_file checks the body against the size the cluster records; this cannot, because the point is not to have the whole thing — and unlike a table, whose truncation leaves a record that does not parse, a file cut short by a mid-stream failure simply ends. A caller who needs certainty compares the reader’s bytes_read against the node’s @uncompressed_data_size — see FileReader for why that gap exists.

§Errors

Returns ClientError if the request fails. Failures during the read arrive from the reader, not from here.

Source

pub fn set_attribute( &self, path: &str, name: &str, value: YsonValue, ) -> Result<()>

Sets a node attribute.

§Errors

Returns ClientError if the request fails.

Source

pub fn write_table(&self, path: impl Into<TablePath>, rows: &[u8]) -> Result<()>

Writes rows to a table, replacing its contents.

rows must be a binary YSON list fragment — exactly what a ytsaurus-job worker writes.

A path carrying a read selection — TablePath::columns, TablePath::range, or rich YPath syntax spelled into the path string — is refused locally, before anything is sent. The cluster ignores those on a write and replaces the whole table with a 200 (measured: write_table_rows("//tmp/t[#0:#2]", rows) replaced everything and reported success), and this refusal is what keeps that silent loss unwritable. See TablePath.

§Errors

Returns ClientError::Config if the path carries a read selection, or ClientError if the request fails.

Source

pub fn write_table_with_format( &self, path: impl Into<TablePath>, rows: &[u8], format: &DataFormat, ) -> Result<()>

Writes rows to a table using a shared DataFormat, replacing its contents.

YSON data is a list fragment in the selected representation. Skiff data is a complete schema-described stream; direct table I/O requires exactly one schema with named non-system fields.

§Errors

Returns ClientError if the format is unsupported, the data is not a complete Skiff stream, or the request fails.

Source

pub fn write_skiff_table( &self, path: impl Into<TablePath>, rows: &[u8], format: &SkiffFormat, ) -> Result<()>

Writes a complete Skiff stream to one table, replacing its contents.

format must have exactly one table schema. Its named fields are sent as the rich-path columns projection, matching the Go SDK; this is how the proxy maps the positional Skiff tuple to table columns. rows is checked against that schema before the request is made.

§Errors

Returns ClientError if the format is not a direct-table format, the stream is incomplete, or the request fails.

Source

pub fn read_table(&self, path: impl Into<TablePath>) -> Result<Vec<u8>>

Reads a whole table as a binary YSON list fragment.

Reads it into memory: this is for results a launcher inspects, not for bulk export.

The path can select which part of the table to read — TablePath::columns and TablePath::range travel as attributes on it, so three columns of a hundred rows cost three columns of a hundred rows, not the whole table:

let head = client.read_table(TablePath::new("//tmp/log").columns(["host"]).range(0..100))?;

The result is checked to be a complete list fragment. That is not pedantry — the proxy reports a mid-stream failure in a trailer this client cannot see (see the http module), so a truncated body is the symptom that is detectable, and returning it as success would hand the caller a silently short table.

§Errors

Returns ClientError if the request fails or the stream is truncated.

Source

pub fn read_table_with_format( &self, path: impl Into<TablePath>, format: &DataFormat, ) -> Result<Vec<u8>>

Reads a whole table using a shared DataFormat.

The returned bytes are a YSON list fragment or a complete Skiff stream, according to format. The response is checked for truncated records before it is returned.

§Errors

Returns ClientError if the format is unsupported, the response is incomplete, or the request fails.

Source

pub fn read_skiff_table( &self, path: impl Into<TablePath>, format: &SkiffFormat, ) -> Result<Vec<u8>>

Reads one table as a complete Skiff stream.

format must have exactly one table schema. Its named fields select the table columns and determine the bytes returned — which is why a path that also names columns is refused. That covers both spellings, TablePath::columns and {…} in the path string, because the format’s fields become a columns attribute here whether the caller named one or not.

What that costs is a silently ignored filter, not a corrupt decode. Measured, the synthesised attribute wins: <columns=[n]>"//tmp/t{k}" answered with column n. A Skiff read therefore still receives exactly the columns its format names, and the tuple stays aligned — but the {…} the caller wrote is discarded without a word, at 200. Refusing is how they get to hear about it. A path string opening with <…> is refused one step removed: this client cannot parse the block to see whether it names columns as well.

Row selections are not column selections and are not refused. A TablePath::range combines, and so does a range spelled into the string — measured, <columns=[n]>"//tmp/t[#0:#2]" answered 200 with rows 0-1 carrying only n. Ranges pick rows, the schema picks columns.

The response is decoded to its end before being returned so a truncated Skiff stream is never reported as a successful table read.

§Errors

Returns ClientError if the format is not a direct-table format, the path also selects columns — through TablePath::columns or as {…} in its string — the path string opens with an attribute block, the response is incomplete, or the request fails.

Source

pub fn write_table_rows<T, I>( &self, path: impl Into<TablePath>, rows: I, ) -> Result<()>
where T: Serialize, I: IntoIterator<Item = T>,

Writes rows to a table from anything that yields them.

The rows are Rust values; the encoding is this crate’s problem, which is the difference between this and Client::write_table:

#[derive(serde::Serialize)]
struct Contact<'a> {
    name: &'a str,
    email: &'a str,
    age: i64,
}

client.write_table_rows("//tmp/contacts", (0..100).map(|n| Contact {
    name: "Gordon Freeman",
    email: "gordon@black-mesa.example",
    age: 27 + n,
}))?;

It takes an iterator rather than a slice because the encoder sits inside the request body: rows are serialised a bufferful at a time as the connection asks for bytes, so a million rows cost one buffer rather than a million rows’ worth of memory, and the caller never has to materialise them either.

Replaces the table’s contents, as Client::write_table does — and refuses a path carrying a read selection before anything is sent, for the reason given there.

§Errors

Returns ClientError::Config if the path carries a read selection, ClientError::Decode naming the row if one cannot be serialised — the write fails rather than sending the rows before it — or ClientError if the request fails.

Source

pub fn read_table_rows<T: DeserializeOwned>( &self, path: impl Into<TablePath>, ) -> Result<Vec<T>>

Reads a whole table as typed rows.

#[derive(serde::Deserialize)]
struct Contact {
    name: String,
    age: i64,
}

for contact in client.read_table_rows::<Contact>("//tmp/contacts")? {
    println!("{} is {}", contact.name, contact.age);
}

Rows are owned, and the whole table is read before any of it is returned — this is Client::read_table with the decoding done, and it inherits the same purpose: results a launcher inspects. For a table that does not fit, or for rows borrowed from the buffer they arrived in, Client::read_table_streaming feeds ytsaurus_job::JobReader.

Columns the type does not mention are ignored, so a struct naming two columns of a twenty-column table is a projection rather than an error — but the whole row still crosses the wire and is decoded before the projection happens. TablePath::columns moves the projection to the cluster, and TablePath::range does the same for rows:

let some: Vec<Contact> = client.read_table_rows(
    TablePath::new("//tmp/contacts").columns(["name", "age"]).range(0..100),
)?;
§Errors

Returns ClientError if the request fails, the stream is truncated, or a row does not match T.

Source

pub fn get_as<T: DeserializeOwned>(&self, path: &str) -> Result<T>

Reads a node, or an attribute, into a Rust type.

Client::get hands back a YsonValue to walk; this hands back the shape you were going to walk it into:

#[derive(serde::Deserialize)]
struct Cluster {
    #[serde(rename = "type")]
    node_type: String,
    creation_time: String,
    account: String,
}

let root: Cluster = client.get_as("//@")?;
println!("the cluster was created at {}", root.creation_time);

Attributes the type does not mention are ignored, which is what makes //@ — a node with dozens of them — worth asking about at all.

§Errors

Returns ClientError if the request fails or the answer does not fit T.

Source

pub fn read_table_streaming( &self, path: impl Into<TablePath>, ) -> Result<TableReader>

Reads a table as a stream, without holding it.

The same bytes Client::read_table returns — a binary YSON list fragment — arriving as they come off the connection, so the table’s size stops being the program’s memory ceiling.

What comes out is what a job reads on fd 0, so the same decoder handles both:

let mut reader = ytsaurus_job::JobReader::binary(client.read_table_streaming("//tmp/big")?);

let mut rows = 0_u64;
while let Some(event) = reader.next_event()? {
    if matches!(event, ytsaurus_job::Event::Row(_)) {
        rows += 1;
    }
}

Client::read_table checks that what came back is a complete fragment; this cannot, because it never has the whole thing. A fragment cut short instead leaves a record that does not parse, and the decoder fails on it — see TableReader for why that is the same protection rather than none.

The path can carry a read selection — TablePath::columns and TablePath::range — which is worth the most here of anywhere: a streaming read exists because the table is too big to hold, and a selection is how most of it never arrives at all.

§Errors

Returns ClientError if the request fails. Failures during the read arrive from the reader, not from here.

Source

pub fn write_table_streaming( &self, path: impl Into<TablePath>, rows: impl Read, ) -> Result<()>

Writes a table from a stream, without holding it.

rows is read to its end and sent as it is read, so the rows can come from a file, a pipe, or something that generates them — anything that is a Read. The bytes are a binary YSON list fragment, exactly as Client::write_table expects them.

client.create("table", "//tmp/big")?;
client.write_table_streaming("//tmp/big", std::fs::File::open("rows.yson")?)?;

This is one attempt and can never be more: a reader that has been consumed cannot be sent again. That agrees with the retry rules — heavy commands are not repeated — and a transaction is what makes such a write safe to fail.

§Errors

Returns ClientError::Config if the path carries a read selection — see Client::write_table — or ClientError if the request fails, including when rows itself fails to read.

Source

pub fn start_map(&self, spec: &MapSpec) -> Result<String>

Starts a map operation, returning its ID.

§Errors

Returns ClientError if the request fails.

Source

pub fn start_map_reduce(&self, spec: &MapReduceSpec) -> Result<String>

Starts a map-reduce operation, returning its ID.

§Errors

Returns ClientError if the request fails.

Source

pub fn start_reduce(&self, spec: &ReduceSpec) -> Result<String>

Starts a reduce operation over sorted input, returning its ID.

The input tables must already be sorted by a column set beginning with the spec’s reduce_by; the cluster refuses the operation otherwise. Client::start_sort is how they get that way.

§Errors

Returns ClientError if the request fails.

Source

pub fn start_sort(&self, spec: &SortSpec) -> Result<String>

Starts a sort operation, returning its ID.

§Errors

Returns ClientError if the request fails.

Source

pub fn start_vanilla(&self, spec: &VanillaSpec) -> Result<String>

Starts a vanilla operation, returning its ID.

Jobs with no input tables: a distributed process, a side-car computation, anything that is not a transformation of a table.

§Errors

Returns ClientError::Config if two tasks share a name, and ClientError if the request fails.

Source

pub fn start_merge(&self, spec: &MergeSpec) -> Result<String>

Starts a merge operation, returning its ID.

A MergeMode::Sorted merge does not need MergeSpec::with_merge_by: measured against a cluster, one sent without it is accepted and the key is taken from the sort columns the inputs already carry, with the output coming back sorted by them. Naming the columns is how to merge by fewer of them than the inputs are sorted by, or to state the assumption where a reader can see it.

§Errors

Returns ClientError if the request fails — including when a sorted merge’s inputs are not sorted, which only the cluster can tell.

Source

pub fn start_erase(&self, spec: &EraseSpec) -> Result<String>

Starts an erase operation, returning its ID.

§Errors

Returns ClientError if the request fails.

Source

pub fn start_remote_copy(&self, spec: &RemoteCopySpec) -> Result<String>

Starts a remote-copy operation, returning its ID.

§Errors

Returns ClientError if the request fails.

Source

pub fn start_operation( &self, kind: OperationType, spec: &YsonValue, ) -> Result<String>

Starts an operation from a spec built by hand.

The escape hatch for anything MapSpec and MapReduceSpec do not model; build the spec with yson_build.

§Errors

Returns ClientError if the request fails.

Source

pub fn start_operation_with( &self, kind: OperationType, spec: &YsonValue, mutation_id: &MutationId, ) -> Result<String>

Starts an operation under a mutation ID you control.

start_operation already tags its own retries with a fresh MutationId, so a retried start never leaves two operations running. This is for the guarantee a single process cannot give itself: persist the ID, and after a crash the same call returns the operation that was already started instead of starting a second one.

The cluster remembers a mutation ID for five to ten minutes, so this is a guard against a crash-and-restart, not a permanent key.

§Errors

Returns ClientError if the request fails.

Source

pub fn abort_operation(&self, id: &str, reason: Option<&str>) -> Result<()>

Stops an operation that is still running.

The counterpart to starting one, and the reason it is worth having: a launcher that gives up — an interrupted wait_for_operation, a failed step further down the script — otherwise leaves the operation running on the cluster, spending quota on a result nobody will read.

reason is put in the operation’s error document, under the cluster’s own Operation aborted by user request, so whoever finds the aborted operation later is told who stopped it and why. Pass None to say nothing.

By the time this returns the operation is already aborted: the call takes a few hundred milliseconds, and the state has changed within it. The aborting state exists but no caller of this can observe it.

This is not idempotent, unlike Transaction::abort. Once the scheduler has let go of an operation it answers No such operation, and it lets go as soon as the first abort is accepted — so a second abort is an error rather than a shrug, even for an operation that was still running a moment ago. An operation that finished by itself can still be aborted for the short while the scheduler keeps it, so this is not a reliable way to ask whether one has finished either.

Sent once, and never retried, which is the other side of the same coin. abort_operation is a scheduler command and the master’s mutation cache does not cover it: a retry after a lost answer would be told No such operation and would report a successful abort as a failed one. A transport error here means the request may or may not have arrived, and the honest thing is to say so rather than to guess.

§Errors

Returns ClientError if the request fails, including when the scheduler no longer has the operation.

Source

pub fn suspend_operation( &self, id: &str, abort_running_jobs: bool, ) -> Result<()>

Pauses a running operation.

Its jobs stop being scheduled; what is already running keeps running unless abort_running_jobs says otherwise, in which case the work those jobs had done is lost and will be done again after Client::resume_operation.

Suspension is not a state. A suspended operation still answers running to Client::operation_state — the cluster reports it in a separate suspended attribute, which is what Client::operation_suspended reads. Verified on a local cluster, and it is the sort of thing a poll loop gets wrong forever.

Unlike its counterpart, this one is idempotent: suspending a suspended operation answers {}, so it is retried like a read. That holds only while the scheduler still has the operation — once it has let go, this answers No such operation like every other command here.

§Errors

Returns ClientError if the request fails, including when the scheduler no longer has the operation.

Source

pub fn resume_operation(&self, id: &str) -> Result<()>

Lets a suspended operation run again.

Sent once, and never retried. Where Client::suspend_operation is idempotent, this is not: an operation that is not suspended answers code 201, Operation is in "running" state. A retry after a lost answer would therefore report a resume that worked as a failure — the same trap Client::abort_operation describes.

§Errors

Returns ClientError if the request fails, including when the operation was not suspended.

Source

pub fn complete_operation(&self, id: &str) -> Result<()>

Finishes an operation early, keeping what it has produced.

The difference from Client::abort_operation: an aborted operation’s output tables are discarded, a completed one’s are published. This is how a long-running vanilla operation is stopped successfully — it ends as completed, and Client::wait_for_operation returns Ok.

Sent once, and never retried, for the reason Client::abort_operation gives: the second one is answered No such operation, so a retry turns a completion that worked into an error.

§Errors

Returns ClientError if the request fails, including when the scheduler no longer has the operation.

Source

pub fn update_operation_parameters( &self, id: &str, parameters: &OperationParameters, ) -> Result<()>

Changes a running operation’s scheduling parameters.

The pool it competes in and the share it gets, while it runs — the one thing about a started operation that is not fixed. See OperationParameters.

client.update_operation_parameters(
    &id,
    &OperationParameters::new().with_pool("interactive").with_weight(2.0),
)?;

The parameters go in the request’s parameters, not its body: the cluster’s registry declares this command’s input as null, whatever the command reference says. It answers with an empty body rather than the {} its neighbours send.

Repeated freely, because it assigns rather than increments: sending the same update twice leaves the operation where the first one put it. As with Client::suspend_operation, that holds only while the scheduler still has the operation — if the answer to the first send is lost and the operation ends during the backoff, the retry is answered No such operation and this returns an error for an update that was applied.

§Errors

Returns ClientError::Config if parameters would change nothing — the cluster accepts an empty update and does nothing, which hides the mistake where it was made — and ClientError if the request fails.

Source

pub fn list_operations(&self, filter: &OperationFilter) -> Result<OperationList>

Lists operations the cluster knows about.

let mine = client.list_operations(
    &OperationFilter::new().with_user("robot-loader").with_state("running"),
)?;

for operation in &mine.operations {
    println!("{} {} {}", operation.id, operation.kind, operation.state);
}

The scheduler only holds operations it has not let go of. Anything older lives in the operations archive, which OperationFilter::with_archive asks for — and which a local cluster does not have.

§Errors

Returns ClientError if the request fails or the response cannot be decoded.

Source

pub fn list_operation_events(&self, id: &str) -> Result<Vec<OperationEvent>>

An operation’s event log.

Empty on a cluster with no operations archive. The command is registered everywhere and answers with an empty list there, rather than with an error — verified on a local cluster, where it is always empty.

§Errors

Returns ClientError if the request fails or the response cannot be decoded.

Source

pub fn attach_operation(&self, id: impl Into<String>) -> Operation

A handle on an operation that is already running.

The reattach door — C++’s AttachOperation, Go’s Track(id). Nothing is sent: an id and a client is all an Operation is, so this cannot fail and does not check that the operation exists. The first command through the handle finds that out.

// A supervisor restarts and picks up where it left off.
let op = client.attach_operation(std::fs::read_to_string("run.id")?);
op.wait()?;

The id is trimmed, for the reason the token file is: the documented way to get one here is out of a file, echo $ID > run.id writes a newline, and an id carrying one is answered No such operation by an error that never mentions whitespace.

Source

pub fn get_operation(&self, id: &str, attributes: &[&str]) -> Result<YsonValue>

The whole document the cluster keeps about an operation.

attributes names what to fetch — state, progress, result, runtime_parameters, spec. An empty slice asks for everything, which is rarely what anyone wants: the full document for a trivial vanilla operation measured 119 KB on a local cluster, most of it the resolved spec and the progress tree. Naming attributes is the normal case, and the narrow readers — Client::operation_state, Client::job_statistics, Client::operation_result_error — are each one attribute of this.

let doc = client.get_operation(&id, &["state", "start_time", "suspended"])?;
§Errors

Returns ClientError if the request fails or the answer cannot be decoded.

Source

pub fn get_operation_by_alias( &self, alias: &str, attributes: &[&str], ) -> Result<YsonValue>

The same, for an operation found by the alias its spec gave it.

An alias is a name a launcher chooses — *nightly-load — set in the spec’s alias field, and the leading * is the cluster’s requirement, not this crate’s. Without it, an alias set at launch could never be looked up again.

The request carries include_runtime, because the cluster refuses the lookup without it: “Operation alias cannot be resolved without using runtime information”. That also bounds what this can find — an alias is resolved from what the scheduler still holds, falling back to the operations archive, so an alias whose operation finished long ago is found only on an installation that has an archive.

§Errors

Returns ClientError if the request fails — including when no operation has that alias — or if the answer cannot be decoded.

Source

pub fn operation_state(&self, id: &str) -> Result<String>

Fetches an operation’s current state, e.g. running or completed.

A suspended operation still reports running. See Client::operation_suspended, or Client::operation_status for both in one request.

§Errors

Returns ClientError if the request fails.

Source

pub fn operation_suspended(&self, id: &str) -> Result<bool>

Whether an operation is paused.

The question Client::operation_state does not answer: the cluster keeps suspension in its own attribute and leaves the state at running, so a loop that watches the state alone will wait out a paused operation without ever saying why.

An operation whose document does not carry the attribute is not suspended, rather than an error: the scheduler reports it for what it still holds, and one resolved out of the operations archive may not carry it at all.

§Errors

Returns ClientError if the request fails, or if the attribute is there and is not a boolean.

Source

pub fn operation_status(&self, id: &str) -> Result<OperationStatus>

An operation’s state and whether it is paused, in one request.

The pair a poll loop actually needs. Asking them separately is two round trips for two attributes of one document, and a loop that asks only for the state cannot tell a running operation from a paused one — they both say running.

let status = client.operation_status(&id)?;
if status.suspended {
    println!("paused — it will sit at {} until it is resumed", status.state);
}
§Errors

Returns ClientError if the request fails or the answer cannot be decoded.

Source

pub fn custom_statistics(&self, operation_id: &str) -> Result<YsonValue>

The custom statistics an operation’s jobs reported.

Returns the custom subtree of the operation’s job statistics, keyed by the names the jobs used. Each leaf is an aggregate — sum, count, min, max — over the jobs that reported it, so a per-row counter comes back as one number for the whole operation. Client::statistic_sum pulls a single total out of it.

Empty if no job reported anything.

§Errors

Returns ClientError if the request fails.

Source

pub fn job_statistics(&self, operation_id: &str) -> Result<YsonValue>

Everything the scheduler recorded about an operation’s jobs.

The whole job_statistics tree, custom and built-in alike. Client::job_statistic_sum is the way to read one number out of it; this is for looking around, which is how anyone finds out what a cluster actually reports.

§Errors

Returns ClientError if the request fails.

Source

pub fn job_statistic_sum( &self, operation_id: &str, path: &str, ) -> Result<Option<i64>>

The total of one built-in job statistic, e.g. time/exec.

The cluster’s own statistics nest by path component, where a custom name keeps its slash as one key — the two are stored differently, which is why they are read differently:

custom:    {"rows/rejected" = {"$"  = {completed = {map = {sum=3}}}}}
built-in:  {time = {exec    = {"$$" = {completed = {map = {sum=744}}}}}}

Note the separator differs too — $$ rather than $. Both are accepted here, because that difference is not something a caller should have to know.

Totalled over completed jobs across job types, as Client::statistic_sum does, and None when the cluster reports nothing under that path — which is not the same as zero. A local cluster reports nothing under user_job/cpu, for instance.

§Errors

Returns ClientError if the request fails.

Source

pub fn statistic_sum( &self, operation_id: &str, name: &str, ) -> Result<Option<i64>>

The total of one custom statistic over an operation’s completed jobs.

name is exactly what the job called it, slashes included: the cluster keeps rows/rejected as one key rather than nesting it.

Only completed jobs are counted. An aborted job’s work is done again by its replacement, so including it would count the same rows twice. Job types are summed together, so a map-reduce reporting one name from both phases gives the operation’s total.

None means no job reported that name — which is not the same as zero.

§Errors

Returns ClientError if the request fails.

Source

pub fn wait_for_operation(&self, id: &str) -> Result<()>

Polls until the operation reaches a terminal state.

A suspended operation never reaches one, and this says so rather than sitting there: suspension is not a state, so a paused operation goes on answering running for as long as it is paused. The progress line reports it, which is the difference between a wait that looks hung and one that names what it is waiting for. Resuming it — from another process, or from the one that paused it — is what ends the wait.

§Errors

Returns ClientError::OperationFailed if it ends as anything other than completed, or ClientError if polling itself fails.

Source

pub fn operation_result_error(&self, id: &str) -> Result<Option<String>>

Why an operation ended as it did, in the cluster’s words.

None for one that succeeded, and for one that has not finished. This is what ClientError::OperationFailed carries, and what reads back the reason given to Client::abort_operation: the reason is folded into the operation’s error document rather than kept beside it, so this is how to find out who stopped an operation and why.

Flattened to the outer message plus the innermost one, because the outer message of a YTsaurus error is a category and the cause is at the bottom.

§Errors

Returns ClientError if the operation cannot be looked up, or if its answer cannot be decoded.

Source

pub fn list_jobs( &self, operation_id: &str, state: Option<&str>, limit: u32, ) -> Result<Vec<JobInfo>>

Lists an operation’s jobs.

state filters by job state — failed, completed, running, … — and limit caps how many come back.

The YTsaurus documentation warns that list_jobs can put significant load on a cluster and asks that it not be part of a workflow without an administrator’s approval. This client calls it once per failed operation, with a small limit; keep to that shape.

§Errors

Returns ClientError if the request fails or the response is not the documented {jobs=[…]}.

Source

pub fn get_job(&self, operation_id: &str, job_id: &str) -> Result<JobInfo>

Fetches one job of an operation.

What Client::list_jobs reports for a job it lists, asked for by id — and the way to look at a job whose id came from somewhere else, a log line or the web interface, without listing every job of the operation.

The cluster answers with the job document unwrapped, and calls the id job_id where list_jobs calls it id; both are read here, so the JobInfo that comes back is the same shape either way.

§Errors

Returns ClientError if the request fails, or if the answer names no job — which is what an unknown job id looks like.

Source

pub fn get_job_input( &self, operation_id: &str, job_id: &str, ) -> Result<ResponseReader>

Streams the input a job was given.

The rows the cluster fed to that one job, in the format its spec asked for — which is how a job that failed on one row is reproduced on a desk rather than on the cluster.

This is a heavy command whose answer is the data, so it streams: nothing here holds the job’s input, and on an installation that separates light and heavy proxies it is sent to the heavy one.

A job with no input never answers. Measured against a local cluster: the request for a vanilla job’s input sat for 30 seconds without a byte. A vanilla operation has no input tables, so there is nothing for the cluster to send and it does not say so; ask this only of a job that reads something.

§Errors

Returns ClientError if the request fails. Failures during the read arrive from the reader, for the reason ResponseReader describes.

Source

pub fn get_job_stderr( &self, operation_id: &str, job_id: &str, ) -> Result<Vec<u8>>

Fetches what a job wrote to stderr.

Returns raw bytes: stderr is whatever the process wrote, not necessarily UTF-8. Empty if the cluster saved nothing — stderr is kept for failed jobs and, when the spec asks for it, for successful ones.

This is a heavy command, so on an installation that separates light and heavy proxies it goes to the heavy one, like a table read.

§Errors

Returns ClientError if the request fails.

Source

pub fn raw_command( &self, method: Method, command: &str, params: &YsonValue, payload: Option<&[u8]>, ) -> Result<Vec<u8>>

Sends a command this crate does not model, and hands back the answer.

Every other method here is a command the crate has an opinion about: parameters built for you, the response decoded into a type. This is the door to the rest of API v4 — the commands this crate has not grown yet, and the ones it never will. It is the same door Client::start_operation opens for a hand-built spec, widened from one command to all of them, and it means the answer to “can I do X against my cluster?” stops being “fork the crate”.

params is the X-YT-Parameters dict — build it with yson_build. payload is the request body, for a command that takes one. What comes back is the response body, exactly as the proxy sent it; API v4 wraps a structured answer in a one-key dict, so most commands answer {key=…} in text YSON.

let client = Client::from_env()?;

// `get_supported_features` is not modelled here and takes no
// parameters. It answers with what this cluster's build can do —
// codecs, compression, primitive types — which is exactly the question
// a crate that models a quarter of the API cannot answer for you.
let body = client.raw_command(
    Method::Get,
    "get_supported_features",
    &yson_build::empty_map(),
    None,
)?;

println!("{}", String::from_utf8_lossy(&body));
§What this still does for you

Everything that is not about the command’s meaning: the token, the timeout, TLS, the header encoding, the X-YT-Error check that turns a cluster failure into a ClientError::Cluster with the innermost message — and the client’s transaction. A raw command is stamped with transaction_id like every other, so a command sent through Transaction is in that transaction rather than quietly outside it. The exceptions are the same: a command that names its own transaction keeps it, and the scheduler commands are not stamped at all.

§What it does not

It is sent once, and to the configured address. A command this crate does not model cannot be assumed non-mutating, and a retry that applied an unknown mutation twice would be a far worse failure than one lost to a flaky proxy — so the default is Repeatable::Never and the retry policy is ignored here, whatever it says.

Never is the safe answer for repeating, and it is the wrong answer for routing: it sends the command to the address the client was configured with, which on an installation that separates proxy roles is a control proxy that will not serve a heavy one. A raw write_file sent this way is refused with Control proxy may not serve heavy requests with input data, and a raw read_file is answered with a 307 to a data proxy. Client::raw_command_with is where a caller who knows the command is heavy says Repeatable::Heavy and gets both halves of that answer at once.

The streaming doors need no such care: Client::raw_command_streaming and Client::raw_command_upload are heavy by construction, because streaming is the heavy shape.

Nor does it know the verb: see Method for the cluster’s own rule for picking one.

§Errors

Returns ClientError::Config if command is not a bare command name, if params is not a YSON dict — every command’s parameters are one, and the client adds to them — or if a body is passed with Method::Get, which carries none, so it would be dropped in silence. Otherwise ClientError as any command fails.

Source

pub fn raw_command_with( &self, method: Method, command: &str, params: &YsonValue, payload: Option<&[u8]>, repeatable: Repeatable, mutation_id: Option<&MutationId>, ) -> Result<Vec<u8>>

As Client::raw_command, saying how the command may be repeated.

The judgement this needs is the cluster’s, not a guess: a command declares whether it mutates and whether it is heavy, and Repeatable is how that reaches the retry policy. Repeatable::Freely for a read, Repeatable::WithMutationId for a light mutation the master’s mutation cache covers, Repeatable::Heavy for one that moves table or file data — which also sends it to a proxy that will accept one — Repeatable::Never otherwise.

“Light and mutating” is not by itself enough for a mutation ID: the cache lives in the master, and a command that goes to the scheduler is not covered by it. Verified for abort_operation — a second send of the same ID, flagged as a retry, is answered No such operation rather than with the first response, so the retry turns an abort that worked into an error the caller believes. Whether every scheduler command behaves that way was not checked; treat it as the working assumption and prefer Never when in doubt.

mutation_id is for the guarantee a single process cannot give itself: persist it, and after a crash the same call is deduplicated against the one that already ran instead of applying twice. See MutationId.

An ID given here is stamped on the request whatever repeatable says, including under Repeatable::Never — the two answer different questions. repeatable decides whether this call may be sent twice; a mutation ID decides whether a later call, from a process that has since restarted, is recognised as the same mutation. A command that must not be retried in-process can still be worth making replayable across one, and this is how.

§Errors

As Client::raw_command.

Source

pub fn raw_command_streaming( &self, method: Method, command: &str, params: &YsonValue, ) -> Result<ResponseReader>

Sends a command this crate does not model and hands back its response unread.

For a command whose answer is the data — read_blob_table, anything the cluster declares heavy on the way out. Client::raw_command would put all of it in memory first, which for those is the thing worth avoiding.

// `read_file` has a method now — `Client::read_file_streaming` is
// this call with the parameters written down — and it stays as the
// example because its wire shape is verified against a cluster, where
// an unmodelled command's here would be a guess. The door sends any
// command the same way.
let mut file = client.raw_command_streaming(
    Method::Get,
    "read_file",
    &yson_build::map([("path", yson_build::string("//tmp/worker"))]),
)?;

std::io::copy(&mut file, &mut std::fs::File::create("worker")?)?;

Sent once, and never retried: this is the shape a heavy command takes, and the documentation is explicit that heavy commands are not repeated. It is also sent to a heavy proxy, for the same reason and without asking — a response that is the data is Repeatable::Heavy whatever the command turns out to be called. The request carries no body — Client::raw_command_upload is the other direction.

The streaming timeout applies, so the transfer itself is not on the request clock; see Client::with_timeout.

§Errors

Returns ClientError::Config if command is not a bare command name, and ClientError if the request fails. Failures during the read arrive from the reader, not from here — and a body cut short by a mid-stream failure ends quietly, for the reason ResponseReader describes.

Source

pub fn raw_command_upload( &self, method: Method, command: &str, params: &YsonValue, body: impl Read, ) -> Result<Vec<u8>>

Sends a command this crate does not model, streaming its request body.

The counterpart of Client::raw_command_streaming, for a command that takes an input data stream — the PUT commands, in the cluster’s own rule. body is read to its end and sent as it is read, so what is uploaded never has to fit in memory.

This is one attempt and can never be more: a reader that has been consumed cannot be sent again. A transaction is what makes such a write safe to fail. And it goes to a heavy proxy, as Client::raw_command_streaming does and for the same reason.

§Errors

Returns ClientError::Config if command is not a bare command name, or if the verb is Method::Get, which carries no body. Otherwise ClientError if the request fails, including when body itself fails to read.

Trait Implementations§

Source§

impl Clone for Client

Source§

fn clone(&self) -> Client

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Client

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more