pub struct BatchRequest { /* private fields */ }Expand description
Commands batched to be sent in one round trip.
A launcher that creates a dozen tables one call at a time pays a dozen round trips; batched, it pays one, and gets a dozen answers:
use ytsaurus_client::{BatchRequest, Client};
let mut batch = BatchRequest::new();
for name in ["clicks", "visits", "errors"] {
batch.create("table", &format!("//tmp/pipeline/{name}"));
}
for (name, made) in ["clicks", "visits", "errors"]
.iter()
.zip(client.execute_batch(&batch)?)
{
match made {
Ok(_) => {}
Err(error) => eprintln!("{name}: {error}"),
}
}§The building shape, and why it is a builder
A batch could equally have been a slice of prepared commands. It is a
builder with a typed method per modelled command because the parts are not
free-form: the cluster refuses a command whose output type is a data stream
(the command reference
puts it as light, with null or structured input and output, which
measurably over-states it — get_job_spec is heavy and is taken, and
write_table has tabular input and is taken; see [NOT_A_BATCH_PART]),
and — the half a slice cannot answer — the
retry of the whole batch turns on what the parts are. A typed method
knows its command is a master-side Cypress command, so the batch stays
retriable under a mutation id; BatchRequest::raw cannot know, so it
makes the batch send-once. A slice of prepared commands would have had to
assume one answer for everything, and the safe assumption would have taken
the retry away from the common case. Each typed method sends exactly
the parameters its Client namesake sends, so a call
moved into a batch does not change meaning.
§Parts run in parallel
From the same reference: “The command can (and will be) executed in
parallel. It means that if a set includes both writing to and reading from
the node, the reading result can either be the older value or the updated
one.” Watched happening on a local cluster: a batch that created
//tmp/impl-batch-a and asked exists about it in the same breath was
answered %false — both parts succeeded, in order, and the read simply ran
first. Do not put a part and its consequence in one batch.
§Options
BatchRequest::with_concurrency is the server-side parallelism, and
BatchRequest::with_max_part_size is a client-side split into several
requests — the same pair the C++ client exposes as
TExecuteBatchOptions{Concurrency, BatchPartMaxSize}.
The two option setters take self, and the part adders take &mut self.
They are different jobs and the shapes say so: the options are the request’s
settings, chosen once and up front, so they chain off the constructor and
are gone by the time the batch has a name; the adders are the contents,
added in a loop, so they hand the borrow straight back. Set the options
first and the mix never shows:
let mut batch = BatchRequest::new().with_concurrency(8).with_max_part_size(64);
for index in 0..3 {
batch.create("table", &format!("//tmp/pipeline/t{index}"));
}
assert_eq!(batch.len(), 3);§Executing one twice sends everything twice
Client::execute_batch borrows the batch,
so it is still there afterwards and can be sent again — and doing so is
new work, not a replay. The parts are unchanged, but each execution
mints its own mutation ids, so the cluster has nothing to deduplicate
against and runs every part a second time. What that looks like is the
part’s own business, and measured: a batch of BatchRequest::create_table
answers 501 already exists throughout the second run, a batch of
BatchRequest::remove answers 500, and a batch of
BatchRequest::create answers with the same node ids as the first run
— because create sends ignore_existing, not because anything was
deduplicated. Do not read that last one as a replay: an unchanged answer
from a second execution is the least informative signal here, which is why
a real replay wants Client::execute_batch_with
and a part that has no ignore_existing in it.
The reuse worth having is a batch of reads, or one
rebuilt from BatchRequest::new for the second pass. A replay — the
same mutation deduplicated against the first send — is
Client::execute_batch_with with the
id you kept.
Implementations§
Source§impl BatchRequest
impl BatchRequest
Sourcepub fn new() -> Self
pub fn new() -> Self
An empty batch. Add parts with the typed methods, then hand it to
Client::execute_batch.
Sourcepub fn with_concurrency(self, concurrency: u32) -> Self
pub fn with_concurrency(self, concurrency: u32) -> Self
Caps how many parts the cluster works on at once.
A parameter of the command itself — concurrency, default 50, refused
unless positive (TExecuteBatchCommand::Register in the cluster’s
driver;
the reference
documents both). The documentation’s reason to lower it: “Use this
parameter to avoid exhausting your request rate limit.” Left unset,
nothing is sent and the cluster’s own default applies.
Zero is clamped to one, as RetryPolicy::new
clamps attempts: the cluster refuses concurrency=0 outright, and a
builder that quietly built a refused request would fail at the wrong
end.
Sourcepub fn with_max_part_size(self, parts: usize) -> Self
pub fn with_max_part_size(self, parts: usize) -> Self
Caps how many parts travel in one HTTP request.
A bigger batch is split client-side into several execute_batch
requests, sent one after another with the results stitched back in
order. This is the C++ client’s BatchPartMaxSize, defaults included:
unset, it is concurrency × 5 — 250 when concurrency is unset too
(yt/cpp/mapreduce/interface/client_method_options.h: “If not
specified it is set to Concurrency * 5”).
The trade is the ordinary one. One request is one round trip and one
retryable unit; a split spends a round trip per piece, and a piece that
fails wholesale fails Client::execute_batch
wholesale with the earlier pieces already run — which that method’s
documentation spells out. Zero is clamped to one, because a part size
of nothing sends nothing forever.
Sourcepub fn create(&mut self, node_type: &str, path: &str) -> &mut Self
pub fn create(&mut self, node_type: &str, path: &str) -> &mut Self
Adds a create — the same request Client::create
sends: parents are created and an existing node is accepted.
The part’s answer is {node_id=…}. With ignore_existing in it, a
node that already existed answers with the old node’s id and any
attributes are silently ignored — the same trap
Client::create_table documents, and
the reason BatchRequest::create_table exists beside this.
Sourcepub fn create_table(
&mut self,
path: &str,
schema: &TableSchema,
) -> Result<&mut Self>
pub fn create_table( &mut self, path: &str, schema: &TableSchema, ) -> Result<&mut Self>
Adds a table creation with a schema — the same request
Client::create_table sends, refusals
included.
The schema goes inside attributes, where create reads it; a
top-level schema would be accepted and silently ignored. And unlike
BatchRequest::create this part fails on a path that already
exists, deliberately: the cluster ignores the attributes of a create
it skips, so an ignore_existing spelling would leave the old table
with the old schema under a per-part Ok.
§Errors
Returns ClientError::Config if the schema is one the cluster would
refuse — checked here, when the part is built, so the mistake is
reported once rather than as a per-part error after a round trip.
Sourcepub fn exists(&mut self, path: &str) -> &mut Self
pub fn exists(&mut self, path: &str) -> &mut Self
Adds an exists — as Client::exists.
The part’s answer is {value=%true} or {value=%false} — the key is
value, not the command’s name, exactly as it is outside a batch.
Sourcepub fn get(&mut self, path: &str) -> &mut Self
pub fn get(&mut self, path: &str) -> &mut Self
Adds a get — as Client::get. The part’s
answer is {value=…}.
Sourcepub fn list(&mut self, path: &str) -> &mut Self
pub fn list(&mut self, path: &str) -> &mut Self
Adds a list — as Client::list. The part’s
answer is {value=[…]}, unsorted and — unlike
Client::list — not checked for the
incomplete marker: a batch hands back what each part answered, and
reading the attribute is the caller’s to do if the node may be large.
Sourcepub fn remove(&mut self, path: &str) -> &mut Self
pub fn remove(&mut self, path: &str) -> &mut Self
Adds a remove — as Client::remove: the
node must exist, and a map node must be empty.
Sourcepub fn remove_tree(&mut self, path: &str) -> &mut Self
pub fn remove_tree(&mut self, path: &str) -> &mut Self
Adds a remove of a whole subtree, absent included — as
Client::remove_tree.
Sourcepub fn set_attribute(
&mut self,
path: &str,
name: &str,
value: YsonValue,
) -> &mut Self
pub fn set_attribute( &mut self, path: &str, name: &str, value: YsonValue, ) -> &mut Self
Adds a set of one attribute — what
Client::set_attribute does.
set takes structured input, and inside a batch that input is the
part’s own input field rather than a request body — the
reference’s
own example is a set carried this way, and the driver encodes the
value and sets the part’s input_format itself
(TExecuteBatchCommand::TRequestExecutor::Run). Verified on a local
cluster; the part answers {output={}}.
Sourcepub fn raw(
&mut self,
command: &str,
params: YsonValue,
input: Option<YsonValue>,
) -> Result<&mut Self>
pub fn raw( &mut self, command: &str, params: YsonValue, input: Option<YsonValue>, ) -> Result<&mut Self>
Adds a command this crate does not model.
The escape hatch, as Client::raw_command
is outside a batch — and with the same default and the same
consequence: a batch carrying a raw part is sent once, whatever the
retry policy says, because a command this crate cannot classify may be
mutating somewhere no mutation cache covers, and a replayed batch would
apply it twice. BatchRequest::raw_with is where a caller who knows
the command’s registry bits says otherwise, exactly as
Client::raw_command_with is
outside a batch; Client::execute_batch
documents the retry rule this feeds.
input is for a structured-input command (the rule
BatchRequest::set_attribute describes); commands with no input
stream pass None. Only light commands with null or structured
input and output can be parts at all — and know that a part naming a
command the cluster has never heard of fails the whole batch, not
the part: watched on a local cluster, where {command=frobnicate} was
answered HTTP 400 and Unknown command "frobnicate" with no per-part
results at all. (The driver decides per-part errors only after it has
resolved the command’s descriptor — TRequestExecutor::Run throws
before that on an unknown name.)
A refused batch is not a partly-run batch. Every part runs. The
failure destroys the answers, not the work: the driver collects the
sub-requests into callbacks, runs them all through
CancelableRunWithBoundedConcurrency, and only then calls
.ValueOrThrow() on the collected list — which discards every result
together the moment one of them is the unknown-name throw. Dispatch is
never aborted. Measured five ways on a local cluster, and it is not a
race: [create, frobnicate] created its node; so did
[frobnicate, create] with the bad part first;
[create, frobnicate, create] created both; and at
concurrency=1, where a reader would most expect the damage to stop
early, [frobnicate, create, create] still created both and eight
creates followed by a frobnicate created all eight. Putting the
bad part first does not help, and lowering the concurrency does not
help. A name worth typing here is one you have checked.
The one distinction that does bound the damage is when the request
fails. A batch refused while its parameters are being read never runs
anything: concurrency=0 was answered Validation failed at /concurrency, a part missing its command field and a part whose
parameters were not a dict were both answered Error loading parameter /requests, and in every one of those a create sitting in the same
request left no node behind. A batch that gets as far as executing
applies all of it. Parse-time failures are total; execution-time
failures are total the other way.
§Errors
Returns ClientError::Config if command is not a bare command
name, or if params is not a YSON dict — the same refusals, for the
same reasons, as Client::raw_command —
or if command is one the cluster will not take as a part, by the
data-type rule BatchRequest::raw_with describes.
Sourcepub fn raw_with(
&mut self,
command: &str,
params: YsonValue,
input: Option<YsonValue>,
repeatable: Repeatable,
) -> Result<&mut Self>
pub fn raw_with( &mut self, command: &str, params: YsonValue, input: Option<YsonValue>, repeatable: Repeatable, ) -> Result<&mut Self>
As BatchRequest::raw, saying how the part may be repeated.
The asymmetry this removes: raw hard-codes Repeatable::Never, and
because the batch retries as the most cautious of its parts, one
raw part demotes an otherwise all-read batch to send-once. A raw read —
check_permission, get_supported_features, parse_ypath — is
Repeatable::Freely, and saying so leaves the batch as retriable as
it was. The judgement is the cluster’s, from the same REGISTER_ALL
row Client::raw_command_with reads
it from, and the same caution applies: light and mutating is not
enough for Repeatable::WithMutationId, because the mutation cache
is the master’s and a scheduler command is not in it. Prefer
Repeatable::Never when in doubt — that is why it is what raw
gives you.
A part’s class is combined with the others, never applied alone: the batch is one HTTP request, so it goes out as the most cautious answer among its parts.
§Errors
As BatchRequest::raw, and additionally ClientError::Config for
Repeatable::Heavy, which is not a class a part can have: it asks for
a heavy proxy, and a batch does not go to one.
A name is also refused when the cluster would refuse it as a part. The
cluster’s rule is the command’s data types, not isHeavy: a part is
refused when its registered output type is tabular or binary, or its
input type is binary, and the driver throws before any part runs, so
the whole batch fails and every other part loses its answer. That is
the check [NOT_A_BATCH_PART] makes, whichever class is claimed for the
name — select_rows and lookup_rows are on it, and are the ones a
caller is likeliest to try.
The list is a snapshot of one cluster’s registry, not a promise. A
cluster of another version registers other commands, and a name this
crate has never heard of can still be refused on the wire — as can a
part whose command takes input and is given none
(Command %Qv requires input), which no list can catch because it
depends on the call. What the check buys is the common mistake caught
before the socket, not a guarantee that the batch will be taken.
Separately, this crate refuses the bulk-data commands it lists as heavy
even where the cluster would take them — write_table was measured
being accepted as a part and applying its rows — because a part’s input
travels inline in the batch body to a light proxy, which is not where
this crate sends table or file data.
Sourcepub fn is_empty(&self) -> bool
pub fn is_empty(&self) -> bool
Whether the batch holds no parts. An empty batch is refused by
Client::execute_batch rather than
sent.
Trait Implementations§
Source§impl Clone for BatchRequest
impl Clone for BatchRequest
Source§fn clone(&self) -> BatchRequest
fn clone(&self) -> BatchRequest
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more