Skip to main content

cufflink_migrate/
lock.rs

1//! Advisory-lock keys. Those of a lock only this platform's processes take
2//! are [`platform_key`]s, apart by namespace. The keys a released binary
3//! derives are kept as it derives them, so that it and this one exclude
4//! each other: the legacy runner's table key here, the deploy lock's (a
5//! service id's bits) and the control plane's migration lock (a constant).
6//!
7//! A key hashes each of its parts as its UTF-8 bytes and then `0xff`, the
8//! bytes std's `Hash` for `str` feeds a hasher, spelled out here because
9//! std does not promise them from one Rust release to the next and replicas
10//! built by different toolchains must derive the same key.
11
12use siphasher::sip::SipHasher13;
13use std::hash::Hasher;
14
15/// SipHash-1-3 under `keys` of `parts`, each its bytes and then `0xff`,
16/// which is no byte of UTF-8.
17fn sip13((key0, key1): (u64, u64), parts: &[&str]) -> u64 {
18    parts
19        .iter()
20        .fold(
21            SipHasher13::new_with_keys(key0, key1),
22            |mut hasher, part| {
23                hasher.write(part.as_bytes());
24                hasher.write_u8(0xff);
25                hasher
26            },
27        )
28        .finish()
29}
30
31/// The key a legacy refinery runner takes for `table`:
32/// `(DefaultHasher(table).finish() as i64).abs() % i64::MAX`, where
33/// `DefaultHasher` is SipHash-1-3 with zero keys. Sharing it keeps that
34/// runner and this one from touching one history at once.
35pub fn legacy_table_lock_key(table: &str) -> i64 {
36    (sip13((0, 0), &[table]) as i64).wrapping_abs() % i64::MAX
37}
38
39/// The key of a lock only this platform's processes take, on `name`
40/// within `namespace`.
41pub fn platform_key(namespace: &str, name: &str) -> i64 {
42    sip13(
43        (
44            u64::from_be_bytes(*b"cufflink"),
45            u64::from_be_bytes(*b"locks\0\0\0"),
46        ),
47        &[namespace, name],
48    ) as i64
49}
50
51/// The key of a history only this platform's runners write.
52pub fn platform_lock_key(table: &str) -> i64 {
53    platform_key("migrate", table)
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use std::collections::hash_map::DefaultHasher;
60    use std::hash::Hash;
61
62    fn legacy_runner_key(table: &str) -> i64 {
63        let mut hasher = DefaultHasher::new();
64        table.hash(&mut hasher);
65        (hasher.finish() as i64).abs() % i64::MAX
66    }
67
68    #[test]
69    fn the_legacy_key_is_the_one_the_legacy_runner_derives() {
70        for (table, key) in [
71            ("loan_service.schema_history", 5095852499937019771),
72            ("catalog_service.schema_history", 576944167566361902),
73            ("public.schema_history_deferred", 315364223451433509),
74            ("schema_history", 3568529105731104943),
75            ("", 3476900567878811119),
76        ] {
77            assert_eq!(legacy_table_lock_key(table), key, "{table}");
78            assert_eq!(legacy_runner_key(table), key, "{table}");
79        }
80    }
81
82    #[test]
83    fn a_platform_key_is_its_namespace_and_name_together() {
84        assert_ne!(
85            platform_key("db_roles", "cufflink_acme"),
86            platform_key("migrate", "cufflink_acme")
87        );
88        assert_ne!(platform_key("ab", "c"), platform_key("a", "bc"));
89    }
90
91    #[test]
92    fn the_platform_keys_are_the_ones_every_replica_derives() {
93        assert_eq!(
94            platform_key("db_roles", "cufflink_acme"),
95            7182794317239927779
96        );
97        assert_eq!(
98            platform_lock_key("_cufflink.schema_history"),
99            -1468775211513434424
100        );
101        assert_eq!(
102            platform_lock_key("loan_service.refinery_schema_history"),
103            -8958653408802261385
104        );
105        assert_ne!(
106            platform_lock_key("_cufflink.schema_history"),
107            legacy_table_lock_key("_cufflink.schema_history")
108        );
109    }
110}