Skip to content

Commit ab2f722

Browse files
authored
Merge pull request #518 from wprzytula/downgrading-policy
Added DowngradingConsistencyPolicy
2 parents c449270 + 95866a0 commit ab2f722

File tree

11 files changed

+1047
-91
lines changed

11 files changed

+1047
-91
lines changed

docs/source/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
- [Retry policy configuration](retry-policy/retry-policy.md)
4949
- [Fallthrough retry policy](retry-policy/fallthrough.md)
5050
- [Default retry policy](retry-policy/default.md)
51+
- [Downgrading consistency policy](retry-policy/downgrading_consistency.md)
5152

5253
- [Speculative execution](speculative-execution/speculative.md)
5354
- [Simple](speculative-execution/simple.md)
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Downgrading consistency retry policy
2+
3+
A retry policy that sometimes retries with a lower consistency level than the one initially
4+
requested.
5+
**BEWARE**: this policy may retry queries using a lower consistency level than the one
6+
initially requested. By doing so, it may break consistency guarantees. In other words, if you use
7+
this retry policy, there are cases (documented below) where a read at `Consistency::Quorum` **may
8+
not** see a preceding write at `Consistency::Quorum`. Do not use this policy unless you have
9+
understood the cases where this can happen and are ok with that. It is also highly recommended to
10+
always log the occurrences of such consistency breaks.
11+
This policy implements the same retries than the [DefaultRetryPolicy](default.md) policy. But on top
12+
of that, it also retries in the following cases:
13+
- On a read timeout: if the number of replicas that responded is greater than one, but lower
14+
than is required by the requested consistency level, the operation is retried at a lower
15+
consistency level.
16+
- On a write timeout: if the operation is a `WriteType::UnloggedBatch` and at least one
17+
replica acknowledged the write, the operation is retried at a lower consistency level.
18+
Furthermore, for other operations, if at least one replica acknowledged the write, the
19+
timeout is ignored.
20+
- On an unavailable exception: if at least one replica is alive, the operation is retried at
21+
a lower consistency level.
22+
23+
The lower consistency level to use for retries is determined by the following rules:
24+
- if more than 3 replicas responded, use `Consistency::Three`.
25+
- if 1, 2 or 3 replicas responded, use the corresponding level `Consistency::One`, `Consistency::Two` or
26+
`Consistency::Three`.
27+
28+
Note that if the initial consistency level was `Consistency::EachQuorum`, Scylla returns the number
29+
of live replicas _in the datacenter that failed to reach consistency_, not the overall
30+
number in the cluster. Therefore if this number is 0, we still retry at `Consistency::One`, on the
31+
assumption that a host may still be up in another datacenter.
32+
The reasoning being this retry policy is the following one. If, based on the information the
33+
Scylla coordinator node returns, retrying the operation with the initially requested
34+
consistency has a chance to succeed, do it. Otherwise, if based on this information we know
35+
**the initially requested consistency level cannot be achieved currently**, then:
36+
- For writes, ignore the exception (thus silently failing the consistency requirement) if we
37+
know the write has been persisted on at least one replica.
38+
- For reads, try reading at a lower consistency level (thus silently failing the consistency
39+
requirement).
40+
In other words, this policy implements the idea that if the requested consistency level cannot be
41+
achieved, the next best thing for writes is to make sure the data is persisted, and that reading
42+
something is better than reading nothing, even if there is a risk of reading stale data.
43+
44+
This policy is based on the one in [DataStax Java Driver](https://docs.datastax.com/en/drivers/java/3.11/com/datastax/driver/core/policies/DowngradingConsistencyRetryPolicy.html).
45+
The behaviour is the same.
46+
47+
### Examples
48+
To use in `Session`:
49+
```rust
50+
# extern crate scylla;
51+
# use scylla::Session;
52+
# use std::error::Error;
53+
# async fn check_only_compiles() -> Result<(), Box<dyn Error>> {
54+
use scylla::{Session, SessionBuilder};
55+
use scylla::transport::downgrading_consistency_retry_policy::DowngradingConsistencyRetryPolicy;
56+
57+
let session: Session = SessionBuilder::new()
58+
.known_node("127.0.0.1:9042")
59+
.retry_policy(Box::new(DowngradingConsistencyRetryPolicy::new()))
60+
.build()
61+
.await?;
62+
# Ok(())
63+
# }
64+
```
65+
66+
To use in a [simple query](../queries/simple.md):
67+
```rust
68+
# extern crate scylla;
69+
# use scylla::Session;
70+
# use std::error::Error;
71+
# async fn check_only_compiles(session: &Session) -> Result<(), Box<dyn Error>> {
72+
use scylla::query::Query;
73+
use scylla::transport::downgrading_consistency_retry_policy::DowngradingConsistencyRetryPolicy;
74+
75+
// Create a Query manually and set the retry policy
76+
let mut my_query: Query = Query::new("INSERT INTO ks.tab (a) VALUES(?)");
77+
my_query.set_retry_policy(Box::new(DowngradingConsistencyRetryPolicy::new()));
78+
79+
// Run the query using this retry policy
80+
let to_insert: i32 = 12345;
81+
session.query(my_query, (to_insert,)).await?;
82+
# Ok(())
83+
# }
84+
```
85+
86+
To use in a [prepared query](../queries/prepared.md):
87+
```rust
88+
# extern crate scylla;
89+
# use scylla::Session;
90+
# use std::error::Error;
91+
# async fn check_only_compiles(session: &Session) -> Result<(), Box<dyn Error>> {
92+
use scylla::prepared_statement::PreparedStatement;
93+
use scylla::transport::downgrading_consistency_retry_policy::DowngradingConsistencyRetryPolicy;
94+
95+
// Create PreparedStatement manually and set the retry policy
96+
let mut prepared: PreparedStatement = session
97+
.prepare("INSERT INTO ks.tab (a) VALUES(?)")
98+
.await?;
99+
100+
prepared.set_retry_policy(Box::new(DowngradingConsistencyRetryPolicy::new()));
101+
102+
// Run the query using this retry policy
103+
let to_insert: i32 = 12345;
104+
session.execute(&prepared, (to_insert,)).await?;
105+
# Ok(())
106+
# }
107+
```

docs/source/retry-policy/retry-policy.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ After a query fails the driver might decide to retry it based on its `Retry Poli
44
Retry policy can be configured for `Session` or just for a single query.
55

66
### Retry policies
7-
By default there are two retry policies:
7+
By default there are three retry policies:
88
* [Fallthrough Retry Policy](fallthrough.md) - never retries, returns all errors straight to the user
99
* [Default Retry Policy](default.md) - used by default, might retry if there is a high chance of success
10+
* [Downgrading Consistency Retry Policy](downgrading_consistency.md) - behaves as [Default Retry Policy](default.md), but also,
11+
in some more cases, it retries **with lower `Consistency`**.
1012

1113
It's possible to implement a custom `Retry Policy` by implementing the traits `RetryPolicy` and `RetrySession`.
1214

@@ -47,5 +49,6 @@ prepared.set_is_idempotent(true);
4749
4850
fallthrough
4951
default
52+
downgrading_consistency
5053
5154
```

examples/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ rustyline-derive = "0.6"
1313
scylla = {path = "../scylla", features = ["ssl"]}
1414
tokio = {version = "1.1.0", features = ["full"]}
1515
tracing = "0.1.25"
16-
tracing-subscriber = "0.2.16"
16+
tracing-subscriber = "0.3.14"
1717
chrono = "0.4"
1818
uuid = "1.0"
1919
tower = "0.4"

scylla-cql/src/errors.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ pub enum DbError {
5454
#[error("The submitted query has a syntax error")]
5555
SyntaxError,
5656

57-
/// The query is syntatically correct but invalid
58-
#[error("The query is syntatically correct but invalid")]
57+
/// The query is syntactically correct but invalid
58+
#[error("The query is syntactically correct but invalid")]
5959
Invalid,
6060

6161
/// Attempted to create a keyspace or a table that was already existing
@@ -178,7 +178,7 @@ pub enum DbError {
178178
WriteFailure {
179179
/// Consistency level of the query
180180
consistency: LegacyConsistency,
181-
/// Number of ndoes that responded to the read request
181+
/// Number of nodes that responded to the read request
182182
received: i32,
183183
/// Number of nodes required to respond to satisfy required consistency level
184184
required: i32,
@@ -253,6 +253,10 @@ pub enum BadQuery {
253253
/// Passed invalid keyspace name to use
254254
#[error("Passed invalid keyspace name to use: {0}")]
255255
BadKeyspaceName(#[from] BadKeyspaceName),
256+
257+
/// Other reasons of bad query
258+
#[error("{0}")]
259+
Other(String),
256260
}
257261

258262
/// Error that occurred during session creation
@@ -447,7 +451,7 @@ mod tests {
447451
// - displays error description
448452
// - displays error parameters
449453
// - displays error message
450-
// - indented multiline strings dont cause whitespace gaps
454+
// - indented multiline strings don't cause whitespace gaps
451455
#[test]
452456
fn dberror_full_info() {
453457
// Test that DbError::Unavailable is displayed correctly

0 commit comments

Comments
 (0)