-
Notifications
You must be signed in to change notification settings - Fork 110
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement timestamp generators #1128
base: main
Are you sure you want to change the base?
Conversation
|
80fa6c4
to
3c0da90
Compare
811f0bf
to
1d70873
Compare
1d70873
to
0f151f1
Compare
6d1943a
to
876d544
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do other drivers (beside cpp-driver) have timestamp generators? If so, how do they implement them?
I'm asking because cpp-compatible implementation does not have to reside in Rust Driver: cpp-rust-driver can implement it in its codebase. We should think about what implementation(s) we want to provide to our users, not what cpp-rust-driver needs here.
Did you maybe test how long a call to MonotonicTimestampGenerator::next_timestamp()
takes? The clock has a microsecond resolution and microsecond is a lot of time. It may be possible that if we call the generator often (like in your unit test), then it may always choose last + 1
branch, because we are still in the same microsecond as previous call. In that case after multiple calls the returned value may be far in the future compared to system clock. If there are multiple clients, it may cause issues.
I've also checked Java and Python drivers, they implement it in the exact same way, taking the microsecond time since epoch. Unfortunately, |
876d544
to
feba1aa
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
I don't think connection is the right layer to utilize the generator, imo it is more fitting for the session layer. Look into session.rs file, into functions like
execute
orrun_query
(cc: @wprzytula ) -
Nit: the commit messages have too long first lines, which makes github not render them correctly. Avoid having any lines longer than 70 characters, especially the first line (which should ideally have at most 50 characters).
-
The
MonotonicTimestampGenerator
struct could use more explanation in its doc comment. Please describe what it guarantees, how it behaves (errors, drifting etc). -
On that front, documentation book should also be updated with info about timestamp generators. It should either be a new file in
queries
folder, or a new folder. @wprzytula @muzarski wdyt it the better place?
scylla/src/transport/connection.rs
Outdated
let mut timestamp = None; | ||
if query.get_timestamp().is_none() { | ||
if let Some(x) = self.config.timestamp_generator.clone() { | ||
timestamp = Some(x.next_timestamp().await); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to clone here, a reference should be enough to generate a timestamp.
warning_threshold_us: i64, | ||
warning_interval_ms: i64, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently you use raw integers for those durations, and you treat "0" as a special value.
The less error prone and more Rusty way is to use std::time::Duration
to store thresholds / intervals, and use Option<std::time::Duration>
instead of using a special value.
Ideally, we could have a new directory with a file for each generator. However, if it turns out in multiple very short |
My first thought was to put the timestamp generation in the session layer, however the functions I've changed in |
For normal queries modifying |
Added TimestampGenerator trait and MonotonicTimestampGenerator based on c++ driver's implementation
Also added an ability to set it through Session Builder
The timestamp generator in ConnectionConfig is set in Session::Connect()
Generated timestamp is only set if user did not provide one
feba1aa
to
4ad3cd7
Compare
69b7789
to
a66210e
Compare
/// Basic monotonic timestamp generator. Guarantees monotonicity of timestamps. | ||
/// If system clock will not provide an increased timestamp, then the timestamp will | ||
/// be artificially increased. If the warning_threshold (by default 1 second) is not None | ||
/// and the clock skew is bigger than its value, then the user will be warned about | ||
/// the skew repeatedly, with warning_interval provided in the settings (by default 1 second). | ||
/// warning_interval should not be set to None if warning_threshold is not None, as it is | ||
/// meaningless, if every generated timestamp over the skew threshold should warn, Duration::ZERO | ||
/// should be used instead. | ||
pub struct MonotonicTimestampGenerator { | ||
last: AtomicI64, | ||
last_warning: Mutex<Instant>, | ||
warning_threshold: Option<Duration>, | ||
warning_interval: Option<Duration>, | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔧 Instead of using runtime checks and unwraps to verify that warning_interval is some if warning_threshold is some you can create a struct which will hold those 2 values (as Duration, not Option), and use Option. Then you'll have a compile-time guarantee that either both are present or both are not.
I'm not sure about the best name for this struct. MonotonicTimestampGeneratorWarnConf is a bit long.
/// Basic monotonic timestamp generator. Guarantees monotonicity of timestamps. | ||
/// If system clock will not provide an increased timestamp, then the timestamp will | ||
/// be artificially increased. If the warning_threshold (by default 1 second) is not None | ||
/// and the clock skew is bigger than its value, then the user will be warned about | ||
/// the skew repeatedly, with warning_interval provided in the settings (by default 1 second). | ||
/// warning_interval should not be set to None if warning_threshold is not None, as it is | ||
/// meaningless, if every generated timestamp over the skew threshold should warn, Duration::ZERO | ||
/// should be used instead. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔧 It would be good to warn users that even though this generator prevents 2 queries from the same client from having same timestamps:
- it does nothing to prevent 2 queries from different clients from having the same timestamp
- it may be a potential point of contention: all queries go trough the atomic operations, potentially with a lot of retries (and the number of retries may increase the more queries there are).
🔧 We should actually benchmark the impact of this. @muzarski can we do that easily with cql-stress?
🤔 @smoczy123 An idea: could we provide another, simpler, impl of TimestampGenerator, that just uses SystemTime
, without monotonicity guarantees. It would avoid contention and still allow users to use timestamps from the client machine.
return u_cur; | ||
} else if self.warning_threshold.is_some() | ||
&& last - u_cur > self.warning_threshold.unwrap().as_micros() as i64 | ||
{ | ||
let mut last_warn = self.last_warning.lock().await; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔧 Instead of using unwrap
you can use if let:
else if let Some(warn_threshold) = self.warning_threshold {
// the rest of the logic
}
async fn next_timestamp(&self) -> i64 { | ||
loop { | ||
let last = self.last.load(Ordering::SeqCst); | ||
let cur = self.compute_next(last).await; | ||
if self | ||
.last | ||
.compare_exchange(last, cur, Ordering::SeqCst, Ordering::SeqCst) | ||
.is_ok() | ||
{ | ||
return cur; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 When the client is under high load, won't this approach be a problem?
We should benchmark this, @muzarski could you help @smoczy123 with that?
The alternative approach would be to just put last
under mutex, avoiding the retries. The added benefit of that is that you can store Instant
under Mutex, which would simplify the code in compute_next
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prepared a branch that uses rust-driver version from this PR: https://github.com/muzarski/cql-stress/tree/timestamp-gen.
@smoczy123, you can set the timestamp generator for c-s
frontend in src/bin/cql-stress-cassandra-stress/main.rs
in prepare_run
function. The Session
object is created in this function.
Commands for some simple workloads:
cql-stress-cassandra-stress write n=1000000 -pop seq=1..1000000 -rate threads=20 -node <ip addresses>
cql-stress-cassandra-stress read n=1000000 -pop seq=1..1000000 -rate threads=20 -node <ip addresses>
First one will insert 1M rows to the databse, while the latter reads the rows and validates them. You can play around with the run parameters and options. You can also try running multiple loads simultaneously to simulate multi-client scenario.
To run scylla locally, you can see https://hub.docker.com/r/scylladb/scylla/. Then you can replace <ip addresses>
in the commands above, with your scylla nodes' ips (comma-delimited list of ips).
If you stumble upon any problems, feel free to ping me.
pub struct MonotonicTimestampGenerator { | ||
last: AtomicI64, | ||
last_warning: Mutex<Instant>, | ||
warning_threshold: Option<Duration>, | ||
warning_interval: Option<Duration>, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔
https://docs.rs/tokio/1.24.2/tokio/sync/struct.Mutex.html#which-kind-of-mutex-should-you-use
To me this looks like a case where std mutex is totally appropriate. Actually, I'm not convinced that TimestampGenerator
even has to be an async trait. @wprzytula @muzarski wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. I don't see any point in using asynchronous functions in the area of generating timestamps - these operations should be very quick and nonblocking.
The trait should not be async.
#[tokio::test] | ||
async fn monotonic_timestamp_generator_is_monotonic() { | ||
const NUMBER_OF_ITERATIONS: u32 = 1000; | ||
|
||
let mut prev = None; | ||
let mut cur; | ||
let generator = MonotonicTimestampGenerator::new(); | ||
for _ in 0..NUMBER_OF_ITERATIONS { | ||
cur = generator.next_timestamp().await; | ||
if let Some(prev_val) = prev { | ||
assert!(cur > prev_val); | ||
} | ||
prev = Some(cur); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔧 Can you create another test to verify that it works when used from multiple threads concurrently?
Driver supports all kinds of statements supported by ScyllaDB. The following tables aim to bridge between DB concepts and driver's API. | ||
They include recommendations on which API to use in what cases. | ||
|
||
## Kinds of CQL statements (from the CQL protocol point of view): | ||
## Kinds of CQL statements (from the CQL protocol point of view) | ||
|
||
| Kind of CQL statement | Single | Batch | | ||
|-----------------------|---------------------|------------------------------------------| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Thanks for the changes in this file!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Commit: "docs/source/SUMMARY.md"
❓ Why did you change indentation in docs/source/SUMMARY.md
file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, my bad, automatic markdown linter changes must have gotten into the commit here and in the queries.md and I missed them, should the changes in both of these files be reverted (except the necessary ones of course)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes in queries.md are fine. The formatting change in SUMMARY could be fine too, if there is some reason apart from personal preference to make it - that's why I asked the question. Afaik Markdown doesn't specify how many spaces should the indentation have, and for MdBook the example SUMMARY.md uses 4.
# Timestamp generators | ||
|
||
If you want to generate timestamps on the client side you can provide | ||
a TimestampGenerator to a SessionBuilder when creating a Session. Then | ||
every executed statement will have attached a new timestamp generated | ||
by the provided TimestampGenerator, as longas the statement did not | ||
already have a timestamp provided (e.g. by using the `TIMESTAMP` clause). | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔧 I don't think this is precise. The timestamp will be attached to the query, but the server will ignore it.
It would be good to describe the precedence here:
USING TIMESTAMP
in the query itself- Timestamp manually set on the query
- Timestamp from the generator
## Monotonic Timestamp Generator | ||
|
||
Most basic client-side timestamp generator. Guarantees monotonic timestamps | ||
based on the system clock, with automatic timestamp incrementation | ||
if the system clock timestamp would not be monotonic. If the clock skew | ||
exceeds warning_threshold of the generator (provided in the constructor, 1s by default) | ||
user will be warned with timestamp generation with warning_interval cooldown period | ||
(provided in the constructor, 1s by default) to not spam the user. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔧 I wouldn't call it "most basic" - it is actually quite complicated. The most basic would be the one I proposed in another comment, which just naively uses SystemTime::now
.
To achieve parity with cpp-driver we need to implement client-side timestamp generators.
This pull request adds a TimestampGenerator trait and a MonotonicTimestampGenerator that implements it,
together with an extension to SessionBuilder that provides an ability to set a TimestampGenerator in Session
and use it to generate timestamps.
Fixes #1032
Pre-review checklist
./docs/source/
.Fixes:
annotations to PR description.