use std::env;
use anyhow::{Context, Result};
use bytes::{BufMut, Bytes, BytesMut};
use chorus_client::{
BearerAuth, ClientConfig, GrpcReplicaFactory, RefreshingAuthConfig, SegmentedVolume,
WalEngineConfig, WalSeqNo,
};
use futures::TryStreamExt;
#[tokio::main]
async fn main() -> Result<()> {
let endpoints = required_csv("CHORUS_GCS_ENDPOINTS")?;
let buckets = required_csv("CHORUS_GCS_BUCKETS")?;
let manifest_endpoint = required_var("CHORUS_GCS_MANIFEST_ENDPOINT")?;
let manifest_bucket = required_var("CHORUS_GCS_MANIFEST_BUCKET")?;
anyhow::ensure!(endpoints.len() == 3 && buckets.len() == 3);
let auth = BearerAuth::google_adc(RefreshingAuthConfig::default()).await?;
let mut factories = Vec::with_capacity(3);
for zone in 0..3 {
factories.push(
GrpcReplicaFactory::connect_with_auth(
zone,
&endpoints[zone],
buckets[zone].clone(),
auth.clone(),
)
.await?,
);
}
let manifest_factory =
GrpcReplicaFactory::connect_with_auth(3, &manifest_endpoint, manifest_bucket, auth.clone())
.await?;
let volume = SegmentedVolume::new(
factories,
manifest_factory,
"databases/orders/wal",
ClientConfig::default(),
)?;
let checkpoint_resume = WalSeqNo::record(0);
let mut recovery = volume.recover(checkpoint_resume).await?;
let replay_end = recovery.end;
while let Some(record) = recovery.try_next().await? {
apply_to_database(record.payload.as_ref())?;
let _next_durable_resume = record.next_seqno();
}
let mut wal = recovery.start(WalEngineConfig::default()).await?;
let first_seqno = replay_end;
let completion_a = wal
.enqueue_append(
first_seqno,
encode_transaction(&[b"put customer/7 alice", b"update customer-index/7"]),
)
.await?;
let completion_b = wal
.enqueue_append(
WalSeqNo::record(first_seqno.record_index + 1),
encode_transaction(&[b"debit account/9 50"]),
)
.await?;
let (receipt_a, receipt_b) = match tokio::try_join!(completion_a, completion_b) {
Ok(receipts) => receipts,
Err(error) => {
if error.may_have_committed() {
anyhow::bail!(
"append outcome may have committed; restart recovery before reusing the \
sequence number: {error}"
);
}
return Err(error.into());
}
};
println!(
"committed appends {:?} and {:?}; durable resumes {:?} and {:?}",
receipt_a.seqno,
receipt_b.seqno,
receipt_a.next_seqno(),
receipt_b.next_seqno()
);
println!(
"replayed through exclusive record {}",
replay_end.record_index
);
let report = wal.truncate_before(replay_end).await?;
println!(
"truncation deleted_segments={} deleted_objects={}",
report.deleted_segments, report.deleted_objects
);
wal.shutdown().await?;
Ok(())
}
fn required_var(name: &str) -> Result<String> {
env::var(name).with_context(|| format!("set {name}"))
}
fn required_csv(name: &str) -> Result<Vec<String>> {
env::var(name)
.with_context(|| format!("{name} is required"))
.map(|value| value.split(',').map(str::to_owned).collect())
}
fn apply_to_database(record: &[u8]) -> Result<()> {
println!("apply {}", String::from_utf8_lossy(record));
Ok(())
}
fn encode_transaction(operations: &[&[u8]]) -> Bytes {
let mut encoded = BytesMut::new();
encoded.put_u32(operations.len() as u32);
for operation in operations {
encoded.put_u32(operation.len() as u32);
encoded.extend_from_slice(operation);
}
encoded.freeze()
}