- Miri
- Test for error conditions
- Async/sync chaos Turmoil / shuttle
- Value checks: quickcheck / proptest
- Logic chaos: cargo-mutants
- Loom - all possible and distinguishable concurrent executions
- Kani: all possible and distinguishable inputs (symbolic execution) - use for concurrent access, unsafe code, where bugs are likely
- Know thy self: Capture the entire performance profile of your program. Not just the easy common path.
- Pathological cases:
- Micro and macro: individual don’t get slower, and you want end to end tests.
- Under, at and over capacity: if the system is not loaded, you don’t want to be using X (too many) CPU cores; at capacity, you can tolerate that load. With “overload” of input, the system doesn’t failover.
- On all relevant targets
Are your benchmarks useful?
Let your CI fail on regression. This isn’t easy!
- Don’t use time (incl. ops/second) / Iai-callgrind
- Run old and new interleaved / tango
- Minimise noise / dedicated host + <100% load
Measure all that matters
- Throughput and “Goodput”
- Memory use (avg. and max.)
- Latency Outcomes:
- Simulate real-life inputs
- Measure system outputs
- Compare output to ground truth
- How you benchmark matters: open vs closed vs partly-open respresentative workloads
- What you record matters: mean, median, histogram, cdc
- How you compare matters, y > x is not sufficien
Document decisions taken
- What alternatives were discarded and why?
- What downsides were explicitly accepted and why? No protract method, but make sure there’s something. Try (Y)ARDs?
Document what’s not there, what shortcuts did we take?
- Missing handles of corder-cases:
todo!()
/unimplemented!()
- Future optimisation opportunities
- Absence of an
impl
(likeFrom
)
You’re holding it wrong is not acceptable in these settings
- NewType: so no one can confuse Meters(u64) with Miles(u64)
struct Meters(u64)
- Typestates
Rocket<S>
with an exampleRocket<Ground>
rocket.launch(/* … */) -> Rocket<Air>
- Two-phase structs: TomlConfig vs ResolvedConfig (??)
- Enums over booleans
- Enums for linked arguments
Surprise and misuse go hand in hand
- Clippy is your friend
- The Rust API Guidelines
- If it smells like OOP or Python/Java/C think again - ahh that smell…
- Use Builders, not “Factories”
- Use mutable references to self, not free functions (newbie alert!!)
- Arc everywhere
- Traits with 3+ super traits, trying to “mock” inheritance in a bad way.
Stagnation the root of all evil. There are more of these than you think.
Minimise hazards
- Concrete types
- Prefer
-> impl Trait
- Avoid pub fields
- Use generics for arguments or
impl Traits
for return positions to allow flexibility for future code changes / You’ll be locked into a specific type (such as Vec or HashMap) and to change this you’ll have to break backwards compatibility. Make the API you promise the user is non restrictive on your future code changes as much as possible.
- Prefer
- Public dependencies
- Args, return types, trait implies etc.
- If you take a public dependency and these leak out into the public API of your crate, say module re-exports, return types etc, such as hyper v0.14 what this means is you cannot upgrade to hyper 1 without breaking your consumers.
impl From
- Prefer non-pub inherent methods!
- Trait implementations are always public. If I implement
From serde_yaml::Value
thenserde_yaml
can never be removed from my dependencies ( a breaking change0
Knowledge is everything
Healthy skepticism is a given.
- Then tracking everything: Complete dependency closure and every deployment and device
- Then connecting to known issues, i.e join against RUSTSEC
- Then vetting for unknown issues: cargo-vet public or internal
- Loud reminders when:
- You’re behind
- Dependencies are dead
- Reduce friction to catching up:
- Auto-merging bump PRs
- Budget for maintenance work
- Upstream changes (no forks!)
- Wrap unstable dependencies.
- Applies to rustc version