commit df462caa1bb3ff72c39003d30aa4626264f290e0 Author: Patrick MARIE Date: Sat Feb 20 17:25:07 2021 +0100 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d7568da --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,374 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "addr2line" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" + +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "backtrace" +version = "0.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bgutil-rs" +version = "0.1.0" +dependencies = [ + "cassandra-cpp", + "chrono", + "clap", + "regex", +] + +[[package]] +name = "bitflags" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "cassandra-cpp" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0767df74532058d1be2b1d7cad0b4808bb1abebbb428123b4130378227386d44" +dependencies = [ + "cassandra-cpp-sys", + "decimal", + "error-chain", + "slog", + "time", + "uuid", +] + +[[package]] +name = "cassandra-cpp-sys" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2481c6da4dd98a4ad6156ddb66784622773be605ce2427214c2c0a0a2cda512a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + +[[package]] +name = "clap" +version = "2.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.2.1", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "decimal" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1556196ca80d503ddabe15a2e97004cb7e48a57be565a2e49984d84098cad669" +dependencies = [ + "bitflags 0.7.0", + "gcc", + "libc", + "ord_subset", + "rustc-serialize", + "serde", +] + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "backtrace", + "version_check", +] + +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + +[[package]] +name = "gimli" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" + +[[package]] +name = "hermit-abi" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +dependencies = [ + "libc", +] + +[[package]] +name = "libc" +version = "0.2.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c" + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "miniz_oxide" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" + +[[package]] +name = "once_cell" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" + +[[package]] +name = "ord_subset" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010031fbb4788c3d4b0cdbabfed03b981761727fc064e3d75c90e33a826cab8f" + +[[package]] +name = "regex" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" + +[[package]] +name = "rustc-demangle" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" + +[[package]] +name = "rustc-serialize" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" + +[[package]] +name = "serde" +version = "1.0.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" + +[[package]] +name = "slog" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thread_local" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi", + "winapi", +] + +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + +[[package]] +name = "uuid" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e47d6c7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "bgutil-rs" +version = "0.1.0" +authors = ["Patrick MARIE "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cassandra-cpp = "0.15.1" +chrono = "0.4" +clap = "2.33.3" +regex = "1.4.3" diff --git a/README.md b/README.md new file mode 100644 index 0000000..2bfe8de --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# bgutil-rs + +## Build + +Don't forget to download & install [cassandra-cpp](https://downloads.datastax.com/cpp-driver/centos/8/cassandra/v2.15.3/) & [libuv](https://downloads.datastax.com/cpp-driver/centos/8/dependencies/libuv/v1.35.0/). + +## Run + +```sh +$ cargo build + Finished dev [unoptimized + debuginfo] target(s) in 0.04s + +$ cargo run -- --help +bgutil-rs + +USAGE: + bgutil-rs + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +SUBCOMMANDS: + help Prints this message or the help of the given subcommand(s) + info Information about a metric +``` + +### Info + +```sh +$ cargo run -- info --help +bgutil-rs-info +Information about a metric + +USAGE: + bgutil-rs info + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +ARGS: + the metric +``` + + +## Todo + +* command: read + - async + - human timestamps +* command: list +* command: clean diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..770b772 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,196 @@ +/* + * bgutil-rs + * + * Author: Patrick MARIE + */ +use std::str::FromStr; +use std::convert::TryFrom; + +use cassandra_cpp::*; +use chrono::Utc; +use clap::{App,AppSettings,Arg,SubCommand}; + +mod metric; +mod stage; +use crate::metric::*; +use crate::stage::*; + + +#[allow(dead_code)] +fn describe_result(result: &CassResult) { + println!("Result has {} record(s).", result.row_count()); + println!("Schema is:"); + + for column_id in 0..result.column_count() { + println!("{:?}: {:?}", + result.column_type(column_id as usize), + result.column_name(column_id as usize) + ); + } +} + +fn connect(contact_points: &str) -> Result { + set_level(LogLevel::DISABLED); + + let mut cluster = Cluster::default(); + cluster.set_contact_points(contact_points).unwrap(); + cluster.set_load_balance_round_robin(); + + cluster.set_protocol_version(4)?; + + cluster.connect() +} + +fn fetch_metric(session: &Session, metric_name: &str) -> Result { + let mut query = stmt!("SELECT * FROM biggraphite_metadata.metrics_metadata WHERE name = ?"); + query.bind(0, metric_name)?; + + let result = session.execute(&query).wait()?; + Ok(result.first_row().unwrap().into()) +} + +fn metric_info(session: &Session, metric_name: &str) -> Result<()> { + let metric = fetch_metric(session, metric_name)?; + + println!("{}", metric); + + Ok(()) +} + +fn fetch_points(session_points: &Session, m: &Metric, s: &Stage, time_start: i64, time_end: i64) -> Result<()> { + let table_name = s.table_name(); + + let q = format!( + "SELECT time_start_ms, offset, value FROM biggraphite.{} WHERE metric = ? AND time_start_ms = ? AND offset >= ? AND offset < ? ORDER BY offset", + table_name + ); + + let ranges = TimeRange::new(&s, time_start, time_end).ranges(); + // XXX concurrent + for range in ranges.iter() { + let mut query = stmt!(q.as_str()); + query.bind(0, Uuid::from_str(m.id().as_str())?)?; + query.bind(1, range.0)?; + query.bind(2, range.1 as i16)?; + query.bind(3, range.2 as i16)?; + + let result = session_points.execute(&query).wait()?; + + for row in result.iter() { + let ts : i64 = row.get_column_by_name("time_start_ms".to_string())?.get_i64()?; + let offset : i16 = row.get_column_by_name("offset".to_string())?.get_i16()?; + let value : f64 = row.get_column_by_name("value".to_string())?.get_f64()?; + + let ts = ts / 1000; + let offset : i64 = offset as i64 * s.precision_as_seconds(); + + println!("{:?};{:?}", ts + offset, value); + } + } + + Ok(()) +} + +fn main() -> Result<()> { + let matches = App::new("bgutil-rs") + .setting(AppSettings::SubcommandRequired) + .subcommand(SubCommand::with_name("info") + .about("Information about a metric") + .arg(Arg::with_name("metric") + .help("metric to retrieve info about") + .index(1) + .required(true) + )) + .subcommand(SubCommand::with_name("read") + .about("Read a metric contents") + .arg(Arg::with_name("stage") + .long("stage") + .takes_value(true)) + .arg(Arg::with_name("time-start") + .long("time-start") + .takes_value(true)) + .arg(Arg::with_name("time-end") + .long("time-end") + .takes_value(true)) + .arg(Arg::with_name("metric") + .help("metric to get values") + .index(1) + .required(true) + )) + .get_matches(); + + let contact_points_metadata = "tag--cstars07--cassandra-cstars07.query.consul.preprod.crto.in"; + let contact_points_data = "tag--cstars04--cassandra-cstars04.query.consul.preprod.crto.in"; + + let session_metadata = match connect(contact_points_metadata) { + Ok(session) => session, + Err(err) => { + eprintln!("{:?}", err); + return Ok(()); + } + }; + + let session_points = match connect(contact_points_data) { + Ok(session) => session, + Err(err) => { + eprintln!("{:?}", err); + return Ok(()); + } + }; + + match matches.subcommand_name() { + Some("info") => { + let matches = matches.subcommand_matches("info").unwrap(); + metric_info(&session_metadata, matches.value_of("metric").unwrap())?; + }, + Some("read") => { + let matches = matches.subcommand_matches("read").unwrap(); + let stage = matches.value_of("stage").unwrap_or("11520*60s"); + // XXX: Change default value relative to stage's precision to have more or less data + let time_start = matches.value_of("time-start"); // default now - 1h + let time_end= matches.value_of("time-end"); // default: now + + let time_start = match time_start { + None => Utc::now().timestamp() - 3600, + Some(s) => match s.parse::() { + Ok(n) => n, + Err(_) => { + eprintln!("Could not parse {}", s); + return Ok(()) + } + } + }; + + let time_end = match time_end { + None => time_start + 3600, + Some(s) => match s.parse::() { + Ok(n) => n, + Err(_) => { + eprintln!("Could not parse {}", s); + return Ok(()) + } + } + }; + + let metric_name = matches.value_of("metric").unwrap(); + let metric = fetch_metric(&session_metadata, metric_name)?; + + let available_stages = metric.stages()?; + let stage = Stage::try_from(stage)?; + + if !available_stages.iter().any(|x| *x == stage) { + eprintln!("Could not find any stage matching {}", stage); + return Ok(()); + } + + fetch_points(&session_points, &metric, &stage, time_start, time_end)?; + } + None => { + eprintln!("No command was used."); + return Ok(()); + }, + _ => {} + } + + Ok(()) +} diff --git a/src/metric.rs b/src/metric.rs new file mode 100644 index 0000000..e7243bb --- /dev/null +++ b/src/metric.rs @@ -0,0 +1,103 @@ +/* + * bgutil-rs + * + * Author: Patrick MARIE + */ +use crate::stage::Stage; + +use std::collections::HashMap; +use std::fmt; +use std::convert::TryFrom; + +use cassandra_cpp::Row; + +#[derive(Debug)] +pub struct Metric { + id: String, + name: String, + config: HashMap, + created_on: u64, + updated_on: u64 +} + +impl Metric { + pub fn id(self: &Self) -> &String { + &self.id + } + + pub fn config(self: &Self, name: String) -> Result { + let res = self.config.get(&name); + if let Some(v) = res { + Ok(v.to_string()) + } else { + Err("Invalid key".to_string()) + } + } + + pub fn stages(self: &Self) -> Result, String> { + let mut out = vec![]; + let stages = self.config("retention".to_string()); + + if let Err(err) = stages { + return Err(err); + } + + for stage in stages.unwrap().split(":") { + match Stage::try_from(stage) { + Ok(stage) => out.push(stage), + Err(err) => { + return Err(err.to_string()) + } + }; + } + + Ok(out) + } +} + +impl fmt::Display for Metric { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} created_on:{} updated_on:{}\n{:?}", + self.name, + self.created_on, + self.updated_on, + self.config, + ) + } +} + +impl From for Metric { + fn from(name: String) -> Self { + Metric { + id: String::from(""), + name: name, + config: HashMap::new(), + created_on: 0, + updated_on: 0 + } + } +} + +impl From for Metric { + fn from(row: Row) -> Self { + let config_collection = row.get_column_by_name("config".to_string()).unwrap().get_map().unwrap(); + let mut config : HashMap = HashMap::new(); + config_collection + .map(|(k, v)| config.insert(k.to_string(), v.to_string())) + .count(); + + let created_on = row.get_column_by_name("created_on".to_string()).unwrap(); + let created_on_timestamp = created_on.get_uuid().unwrap().timestamp(); + + let updated_on = row.get_column_by_name("updated_on".to_string()).unwrap(); + let updated_on_timestamp = updated_on.get_uuid().unwrap().timestamp(); + + Self { + id: row.get_column_by_name("id".to_string()).unwrap().get_uuid().unwrap().to_string(), + name: row.get_column_by_name("name".to_string()).unwrap().to_string(), + config: config, + created_on: created_on_timestamp, + updated_on: updated_on_timestamp + } + } +} diff --git a/src/stage.rs b/src/stage.rs new file mode 100644 index 0000000..0d211ca --- /dev/null +++ b/src/stage.rs @@ -0,0 +1,181 @@ +/* + * bgutil-rs + * + * Author: Patrick MARIE + */ +use std::fmt; +use std::convert::TryFrom; + +use regex::Regex; + +#[derive(Debug)] +pub struct Stage { + stage: String, + points: u32, + precision: u32, + factor: char, +} + +impl TryFrom<&str> for Stage { + type Error = &'static str; + + fn try_from(stage: &str) -> Result { + let re = Regex::new(r"^(\d+)\*(\d+)(.)"); + + if let Err(_) = re { + return Err("regex initialisation failed"); + } + + let captures = match re.unwrap().captures(&stage) { + None => return Err("invalid regex capture"), + Some(c) => c, + }; + + let points = captures.get(1).unwrap().as_str().parse::().unwrap(); + + let factor = captures.get(3).unwrap().as_str(); + if factor.len() != 1 { + return Err("invalid factor length") + } + + let factor = factor.chars().nth(0).unwrap(); + + match factor { + 's' | 'm' | 'h' | 'd' | 'w' | 'y' => {}, + _ => { + return Err("invalid precision unit") + } + }; + + let precision = captures.get(2).unwrap().as_str().parse::().unwrap(); + + Ok(Stage { + stage: String::from(stage), + points: points, + precision: precision, + factor: factor, + }) + } +} + +impl Stage { + pub fn precision_as_seconds(self: &Self) -> i64 { + let factor = match self.factor { + 's' => 1, + 'm' => 60, + 'h' => 60 * 60, + 'd' => 60 * 60 * 24, + 'w' => 60 * 60 * 24 * 7, + 'y' => 60 * 60 * 24 * 365, + _ => unreachable!() + }; + + factor * self.precision as i64 + } + + pub fn time_offset_ms(self: &Self, ts: i64) -> (i64, i64) { + let table_row_size_ms = self.table_row_size_ms(); + let time_offset_ms = ts * 1000 % table_row_size_ms; + let time_start_ms = ts * 1000 - time_offset_ms; + + ( + time_start_ms, + time_offset_ms / (self.precision_as_seconds() * 1000) + ) + } + + pub fn table_name(self: &Self) -> String { + // XXX aggregations? + format!("datapoints_{}p_{}{}_0", self.points, self.precision, self.factor) + } + + pub fn table_row_size_ms(self: &Self) -> i64 { + let hour = 3600; + let _max_partition_size = 25000; + let _expected_points_per_read = 2000; + let _min_partition_size_ms = 6 * hour; + + std::cmp::min( + self.precision_as_seconds() * 1000 * _max_partition_size, + std::cmp::max( + self.precision_as_seconds() * 1000 * _expected_points_per_read, + _min_partition_size_ms, + ) + ) + } +} + +impl Clone for Stage { + fn clone(&self) -> Stage { + Stage { + stage: self.stage.to_string(), + ..*self + } + } +} + +impl PartialEq for Stage { + fn eq(&self, other: &Self) -> bool { + self.points == other.points + && self.precision == other.precision + && self.factor == other.factor + } +} + +impl Eq for Stage {} + +impl fmt::Display for Stage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.table_name()) + } +} + +pub struct TimeRange { + stage: Stage, + time_start: i64, + time_end: i64 +} + +impl TimeRange { + pub fn new(stage: &Stage, time_start: i64, time_end: i64) -> Self { + TimeRange { + stage: stage.clone(), + time_start: time_start, + time_end: time_end, + } + } + + pub fn ranges(&self) -> Vec<(i64, i64, i64)> { + let first_offset = self.stage.time_offset_ms(self.time_start); + let last_offset = self.stage.time_offset_ms(self.time_end); + + let mut offset = first_offset.0; + let mut offset_start = first_offset.1; + + let mut out = vec![]; + + while offset != last_offset.0 { + out.push((offset, offset_start, self.stage.table_row_size_ms())); + + offset_start = 0; + offset += self.stage.table_row_size_ms(); + } + + out.push((offset, offset_start, last_offset.1)); + + out + } +} + +impl fmt::Display for TimeRange { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} ({} -> {})", self.stage, self.time_start, self.time_end) + } +} + +#[test] +fn timerange() { + let stage = Stage::try_from("11520*60s").unwrap(); + assert_eq!(vec![(0, 2, 4)], TimeRange::new(&stage, 120, 240).ranges()); + assert_eq!(vec![(0, 2, 11520),(691200000, 0, 4)], TimeRange::new(&stage, 120, 691200 + 240).ranges()); +}