Skip to main content

BatchRequest

Struct BatchRequest 

Source
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

Source

pub fn new() -> Self

An empty batch. Add parts with the typed methods, then hand it to Client::execute_batch.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

pub fn get(&mut self, path: &str) -> &mut Self

Adds a get — as Client::get. The part’s answer is {value=…}.

Source

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::listnot 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.

Source

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.

Source

pub fn remove_tree(&mut self, path: &str) -> &mut Self

Adds a remove of a whole subtree, absent included — as Client::remove_tree.

Source

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={}}.

Source

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.

Source

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.

Source

pub fn len(&self) -> usize

How many parts the batch holds.

Source

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

Source§

fn clone(&self) -> BatchRequest

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 BatchRequest

Source§

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

Formats the value using the given formatter. Read more
Source§

impl Default for BatchRequest

Source§

fn default() -> BatchRequest

Returns the “default value” for a type. 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