Request timeouts
The library sets no timeout on requests by default. A request waits for Firestore for as long as its connection lives, and a connection to a server that stopped answering is closed only by HTTP/2 keep-alive, after about two minutes.
To limit how long one request waits for Firestore, set request_timeout in the options:
use firestore::*;
use std::time::Duration;
fn config_env_var(name: &str) -> Result<String, String> {
std::env::var(name).map_err(|e| format!("{name}: {e}"))
}
async fn example() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let db = FirestoreDb::with_options(
FirestoreDbOptions::new(config_env_var("PROJECT_ID")?)
.with_request_timeout(Duration::from_secs(30)),
)
.await?;
Ok(())
}
What it limits depends on the request:
- unary requests (get, insert, update, delete, list pages, batch writes, transaction commits and the admin API calls): the whole call. The limit is also sent to Firestore as the request deadline;
- streamed reads (queries, aggregations and batch gets): the wait for the stream to start and for each next document, counted only while you are waiting for one, so a slow consumer of a long query is not cut;
- pipeline and SQL reads, on the Enterprise edition: the wait for each response, as for a streamed read;
- pipeline and SQL writes: the wait for each response, for a write that is sent once;
- listeners and the streaming batch writer are not limited, since their streams stay quiet for as long as nothing changes.
A listener resets a stream that sent nothing for its own
idle_timeout, see Listening to document changes.
To use another limit for a part of your code, or none at all, clone the client with clone_with_request_timeout.
Batch writers and transactions created from the clone keep it:
use firestore::*;
use std::time::Duration;
async fn example(db: FirestoreDb) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let report_db = db.clone_with_request_timeout(Some(Duration::from_secs(300)));
let unlimited_db = db.clone_with_request_timeout(None);
Ok(())
}
Retries
The limit applies to each attempt. A request that runs out of it fails with a DatabaseError with the code
DeadlineExceeded and retry_possible set. Reads and simple batch writes retry it:
- gets, lists, list collection IDs and the start of queries, aggregations, pipelines and SQL reads, up to
max_retriestimes; - simple batch writes, until
retry_max_elapsed_time. A retried batch may apply twice, for example anincrement, as it can after a dropped connection.
insert(), update() and delete() return a request timeout that ran out without a retry, see
Single writes.
A read makes max_retries + 1 attempts, and the error that comes back after the last one still has retry_possible set.
A pipeline or SQL write makes one attempt and clears the flag, see
Retries and listeners. A pipeline that a cache session answers sends no
request, so no timeout or retry applies to it.
Batch gets, partition queries, a query or aggregation stream that has already started and transaction commits return the error without a retry. A transaction commit is never retried, because its writes may already be applied.
The reads in the first item also retry a DEADLINE_EXCEEDED that Firestore itself answers with, up to max_retries
times, with or without a request timeout set. Inserts, updates, deletes, batch gets, partition queries and streams that
have already started return it with retry_possible unset.
Single writes
insert(), update() and delete() run with .execute() are sent again up to max_retries times after
RESOURCE_EXHAUSTED and UNAVAILABLE. A connection lost while a request is in flight is retried like UNAVAILABLE.
These are the codes that Google’s Firestore server libraries for Java, Go and Python retry a write on. They send every
single write, a delete included, as a commit, so one list covers all three. The Node.js library also retries ABORTED
on a commit. The library does not, see When a write fails.
Any other code is returned at once, whatever retry_possible says: INTERNAL, ABORTED and DEADLINE_EXCEEDED
included, the request timeout running out among them.
A write that is sent again can find that the first attempt was applied, since UNAVAILABLE and a lost connection do not
say whether it was. Then:
- an
insert()fails withALREADY_EXISTSwhen the first attempt created the document; - an
update()ordelete()with anUpdateTimeprecondition fails withFAILED_PRECONDITION, since the document no longer has that update time. The write went through, and the error reports that it did not; - an
update()withFirestoreWritePrecondition::Exists(false)fails withALREADY_EXISTSwhen the first attempt created the document; - a
delete()withFirestoreWritePrecondition::Exists(true)fails withNOT_FOUNDwhen the first attempt deleted the document; - a
delete()without a precondition finds the document already gone, which is not an error; - an
update()with transformations applies them again: anincrementadds its value twice. Do not use such an update where a repeated increment is wrong; run it in a transaction or setmax_retriesto0.
Writes queued with add_to_transaction or add_to_batch, transaction commits, and pipeline and SQL writes are not
retried this way.
Be aware not to set retry_max_elapsed_time to None on a simple batch writer when you set a request timeout: against
a Firestore that stopped answering, it then retries forever, one timeout per attempt. The 60 second default is checked
between attempts, so the last attempt can still run for a whole timeout past it.
Retry limits
Reads, single writes, simple batch writes and transactions wait the same delays between retries: 500 ms, growing by 1.5 times for each next retry, each delay randomised by up to a third either way. They differ in when they stop:
- reads and single writes stop after
max_retriesofFirestoreDbOptions, 3 by default, with no time limit; - simple batch writes stop after
retry_max_elapsed_timeofFirestoreSimpleBatchWriteOptions, 60 seconds by default, and ignoremax_retries; - transactions stop after
max_retriesofFirestoreTransactionOptions, 4 by default, and, when set, afterretry_max_elapsed_time, which is off by default.