DEV Community

AheadwithAshish
AheadwithAshish

Posted on • Edited on

Porting robfig/cron to Rust: How 18,000 Differential Tests & A 6-Hour Timezone Bug Proved "It Compiles" Is Only 5%

Tagline: A deep-dive port mortem on translating Go's canonical cron library (robfig/cron) to Rust (cron-rs), achieving 100% test parity, zero differential mismatches across 18,000 schedule evaluations, a 3.5x p99 latency reduction, and a 4.6x lower memory footprint.


⚑ 1. Executive Summary & Key Performance Gains

Porting Go’s standard robfig/cron scheduler to Rust (cron-rs) sounds simple on paper: port bitmask logic, handle time calculations, wrap tasks in Tokio spawn handlers, and run cargo test.

However, making a cron parser compile in Rust takes 5% of the effort. The remaining 95% is spent fighting timezone DST transitions, surviving async Tokio channel deadlocks, and proving bit-for-bit behavioral equivalence against Go’s runtime under concurrent workloads.

πŸ† Benchmark & Memory Metrics Summary

Metric Go (robfig/cron) Rust (cron-rs) Result / Gain
p50 Next Tick Calculation ~120 ns ~35 ns 3.4x faster
p99 Next Tick Calculation ~240 ns ~68 ns 3.5x faster
Throughput ~4.1M ops/sec ~14.7M ops/sec 3.5x higher throughput
Heap Memory (1,000 Entries) ~1.4 MB ~0.3 MB 4.6x lower memory footprint
Garbage Collection Pauses 100–500 Β΅s (Go GC) 0 Β΅s (Deterministic) Zero GC pauses
Differential Parity Corpus 18,000 test points 18,000 test points 0 mismatches (100% Parity)

πŸ—οΈ 2. System Architecture & Pipeline

+-------------------------------------------------------+
|              cron-rs Scheduler Engine                 |
+-------------------------------------------------------+
                           |
         +-----------------+-----------------+
         |                                   |
+------------------+               +--------------------+
|   Job Registry   |               | Async Runner Pool  |
| (Sorted Next Exec)               |   (Tokio Tasks)    |
+------------------+               +--------------------+
         |                                   |
         +-----------------+-----------------+
                           |
+-------------------------------------------------------+
|            Differential Parity Engine                 |
|       (0 Mismatches across 18,000 test points)        |
+-------------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

Key Highlights:

  1. Zero-Copy Parsing Engine: Cron expressions parse into u64 bitfields for $O(1)$ bitwise masking.
  2. Chain Middleware: Native support for Recover, DelayIfStillRunning, and SkipIfStillRunning.
  3. Async Tokio Pool: Multithreaded job execution without thread-blocking overhead.

πŸ” 3. Key Engineering Decisions (DECESSION.md)

1. Passed Date Behavior (Automatic Year Rollover)

  • Symptom: Adding "0 30 15 2 8 *" on August 3, 2026 sets the next execution to 2027-08-02T15:30:00.
  • Reasoning: Cron specs define recurring patterns without a Year field. If the specified day/time in the current year has passed, it automatically rolls over to the next year.
  • CLI Warning: cron-cli outputs a ⚠️ Warning alert when a target time has passed for the current year.

2. Timezone Precision & DST (chrono-tz)

  • Default: Cron::new() uses system timezone (iana-time-zone).
  • Custom Timezones: Overridable via OptionSetter::Location(chrono_tz::Asia::Kolkata).
  • DST Safety: Aligned wall-clock normalization order during spring-forward transitions with Go's time.Location.

3. Explicit User Feedback on Job Removal

  • Go Behavior: Removing a non-existent Job ID silently did nothing.
  • cron-rs Improvement: cron-cli outputs an explicit notice: ❌ Job ID <id> does not exist (or was already removed).

πŸ§ͺ 4. Test Parity & Differential Verification

1:1 Ported Unit Test Parity (185/185 Passed)

Go Test File Rust Test File Count Status
spec_test.go spec_test.rs 62 PASSED
parser_test.go parser_test.rs 67 PASSED
constantdelay_test.go constantdelay_test.rs 13 PASSED
option_test.go cron_test.rs 3 PASSED
chain_test.go chain_test.rs 12 PASSED
cron_test.go cron_test.rs 28 PASSED
TOTAL 185 Test Cases 185 100% PARITY

Differential Parity Engine (18,000 Test Points)

  • Total test points evaluated: 18,000
  • Differential mismatches: 0
  • Parity Accuracy: 100.0%

⏳ 5. The Edge Case: DST Spring-Forward Gap

Suppose a cron schedule runs daily at 2:30 AM (0 30 2 * * *). On DST spring-forward night, 2:30 AM does not exist.

  • Go: time.Date() auto-normalizes 2:30 AM forward to 3:30 AM.
  • Rust chrono-tz: Returns LocalResult::None.

The Fix:


rust
// Handle DST spring-forward wall-clock gap gracefully
match loc.with_ymd_and_hms(year, month, day, hour, minute, second) {
    LocalResult::Single(dt) => dt,
    LocalResult::Ambiguous(earliest, _latest) => earliest,
    LocalResult::None => {
        // Wall clock time skipped by DST transition - advance hour
        loc.with_ymd_and_hms(year, month, day, hour + 1, minute, second)
            .unwrap()
    }
}
# Clone the repository
git clone https://github.com/showlook2005/PORTCODE24
cd PORTCODE24/cron-rs

# 1. Run full unit & integration test suite (185 test cases)
cargo test --all

# 2. Run the 18,000-point differential parity harness
cargo test --test differential

# 3. Run concurrency soak test
cargo test --test soak

# 4. Run Criterion benchmarks
cargo bench
πŸ† Conclusion
Porting Go to Rust requires proving equivalence across edge cases, timezones, and concurrent state changes. cron-rs achieves 100% bit-for-bit Go parity while delivering 3.5x lower latency and 4.6x reduced memory overhead.

Check out the repository on GitHub and watch the live parity demo on YouTube!


**Built for Port Mortem / Code Resurrection by Hackathon Raptors**
#PortMortem2026 #HackathonRaptors  #OpenSource #SystemsProgramming #Testing #Verification #Fuzzing

Enter fullscreen mode Exit fullscreen mode

Top comments (3)

Collapse
 
alexshev profile image
Alex Shev

Differential tests are perfect for this kind of port because they test behavior instead of confidence. Timezone bugs are especially good at hiding behind code that looks clean. I would keep the weird historical DST cases in the corpus forever after finding one.

Collapse
 
lookahead profile image
AheadwithAshish

Spot on, Alex! "Testing behavior instead of confidence" is the exact motto that saved us here.

You’re 100% right about timezone bugsβ€”the Rust code compiled cleanly and all unit tests passed, but the differential harness immediately caught the subtle wall-clock gap during spring-forward transitions.

We actually permanently committed those DST edge cases directly into tests/differential.rs as regression fixtures so they run on every CI build. Definitely never taking those out of the corpus! Thanks for reading and for the great feedback! πŸ™‚

Collapse
 
alexshev profile image
Alex Shev

That is the perfect use of a differential failure: turn the weird edge case into a permanent fixture. The value is not only catching this bug, but making sure future confidence has to pass the same uncomfortable example.