Skip to main content

objectstore_inventory_tracker/
lib.rs

1//! Emits per-record inventory change events for shared storage resources.
2//!
3//! Change events are published to the `shared-resources-inventory` Kafka topic. They
4//! include (among other things):
5//! - `shared_resource_id`: identifies the storage backend the record is hosted in. This
6//!   is meant to be, for example, a specific GCS bucket or Bigtable instance.
7//! - `record_id`: identifies each record. `InventoryTracker` populates this with a hash
8//!   of the identifier passed in by the caller.
9//! - `size`: the size of the record in bytes (including metadata).
10//! - `expiration_time`: a timestamp (unixtime microseconds) describing when the record is
11//!   meant to be deleted.
12//!
13//! # Usage
14//!
15//! ```
16//! use objectstore_inventory_tracker::{InventoryTracker, NoopProducer};
17//! use std::time::SystemTime;
18//!
19//! # let producer = NoopProducer;
20//! // Create your `InventoryTracker`. This one is for the `my_gcs_bucket` bucket
21//! // and has a sampling rate of `1.0` (unsampled).
22//! let tracker = InventoryTracker::new(producer, "my_gcs_bucket", 1.0);
23//!
24//! // Emit a message indicating that `storage_key` has been written, 4096 bytes in size.
25//! // Sampling and hashing of the key are handled internally.
26//! let storage_key = "example_feature/org.1/project.1/objects/abc";
27//! tracker.write(storage_key, "example_feature", 4096, SystemTime::now(), None, Some(1), Some(1))?;
28//! # Ok::<(), std::convert::Infallible>(())
29//! ```
30//!
31//! The `"my_gcs_bucket"` string in the above example is meant to correspond to a
32//! label on the specific GCS bucket your service uses to store data. That way, an
33//! inventory derived from your change stream can be joined with billing data to
34//! analyze costs. If you have multiple buckets, or multiple storage backends, it's
35//! recommended that you configure each of them with their own `InventoryTracker`.
36//!
37//! # Sampling
38//!
39//! [`InventoryTracker`] hashes the storage key it gets from the caller and uses part of
40//! the digest to determine whether change messages should be emitted for a given object.
41//! Each [`InventoryTracker`] instance is configured with its own sample rate so services
42//! can enable, disable, or tune sampling as needed.
43//!
44//! Each change message includes the sampling rate that was in effect when the message was
45//! emitted. Downstream consumers can use `1 / sample_rate` as a weight when computing
46//! aggregates to approximate what the unsampled aggregate would have been.
47//!
48//! When sampling is used the resulting dataset will generally be representative. However,
49//! it will not be complete, and with a low sample rate it's more likely that a small
50//! trend or subpopulation of your data will be entirely missing from the sampled dataset.
51//!
52//! # Fail open
53//!
54//! [`Producer::send`] does not block, and it returns an error instead of waiting when the
55//! local queue is full. Callers may count the error and move on.
56//!
57//! Sending is asynchronous, so messages handed over just before a process exits are still
58//! sitting in a local queue. Await [`InventoryTracker::join`] during shutdown to deliver
59//! them; anything still queued when the process goes away is lost.
60//!
61//! Losing change messages causes drift in downstream consumers. If your storage service
62//! expires data, this drift will sort itself out as records age out. If your service
63//! retains data indefinitely, consider writing a periodic reconciliation job.
64//!
65//! # Ordering
66//!
67//! Each change message has a `timestamp` field with microsecond resolution so that
68//! multiple changes to the same record are ordered.
69//!
70//! NOTE: This crate can provide no guarantees that this ordering scheme will always
71//! result in messages being serialized in Kafka in the same order the corresponding
72//! operations were serialized in your service or its storage layer. If this is an issue,
73//! consider writing a periodic reconciliation job or implementing synchronization outside
74//! of this crate.
75
76#![warn(missing_docs)]
77#![warn(missing_debug_implementations)]
78
79mod producer;
80mod record;
81mod tracker;
82
83#[cfg(any(test, feature = "test-utils"))]
84pub mod test_utils;
85
86#[cfg(feature = "kafka")]
87pub mod kafka;
88
89pub use producer::{BoxError, NoopProducer, Producer, SharedProducer};
90pub use record::{InventoryRecord, OpType, epoch_micros};
91pub use tracker::InventoryTracker;