在年初的一篇關于商業開源的博文當中,我介紹了在開發商業軟件的過程中,衍生出開源公共軟件庫的模式。在那篇博文里面,我只是簡單羅列了相關開源庫的名字及一句話總結。近期,我會結合商業開源實踐的最新進展,對其中一些案例做詳細展開。
首先介紹的是?Cronexpr[1],一個小巧的 Rust Crontab 解析庫。其背后的開源模式,我稱之為“涓滴開源”,即將商業軟件依賴的微小但完整的功能模塊開源出來,供其他人使用。
ScopeDB 的實際需求
開源軟件的源頭活水是有人需要。
“只是為了好玩”(Just For Fun)固然可以成為某人一時的動力,但是開源項目能夠長期持續維護,肯定是因為有用戶長期使用,倒推上游保持更新。作為互動的另一環,如果上游不再更新,用戶也會逐漸流失。
Cronexpr 的需求來源于我和幾位伙伴從去年開始開發的?ScopeDB 云數據庫[2]。ScopeDB 是一個商業數據庫,構建在云計算彈性且廉價的資源之上,通過一致的接口體驗和查詢語言將原本疊床架屋的數據流水線,簡化為從應用直接寫入 ScopeDB 后即可執行任意查詢。在實現 ScopeDB 的過程中,我們發現類似數據保留策略的執行、簡單的物化視圖構建,以及數據存儲的整理,都可以用定時后臺作業的模型來建模:
CREATE?JOB archive_table_tSCHEDULE =?'4 2 * * * Asia/Shanghai'NODEGROUP =?'background'
ASDELETE?FROM?tWHERE?created_at <?NOW() -?INTERVAL?'720 hours';
可以看到,這里在后臺作業的調度周期的時候,用的是形如?4 2 * * * Asia/Shanghai
?的 Cron 表達式。這也是定時后臺作業常用的調度邏輯定義方案了。
于是,為了支持解析 Cron 表達式,并在 ScopeDB 調度后臺作業的邏輯當中嵌入 Cron 表達式的計算邏輯,我第一時間想到的就是尋找一個現成的開源庫來解決需求。
Cronexpr 的誕生
很快,我就找到了 Rust 生態里兩個看起來不錯的 Cron 表達式軟件庫:
croner[3]
saffron[4]
其中 croner 看起來是獨立開發者發布的軟件庫,不過當時已經發布了 2.x 版本,今天再看已經發布了 3.x 版本,看起來作者是對軟件的成熟度比較有信心的。saffron 雖然只發了一個 0.1.0 版本,但是它是 Cloudflare 出品,且看起來接口也比較正經,應該也相對成熟。
不過,這兩者都不支持帶時區信息的 Cron 表達式,這對用戶體驗來說有比較大的差別。同時,這兩者對 Cron 表達式解析時的一些實現細節和擴展,都有奇特的“村規”。最后,由于這兩個庫實現時間較早,它們所采用的時間庫是舊的 chrono 庫,而不是現在更可靠的 jiff 庫。至于其他 Cron 表達式相關的開源庫,要么是一看就不靠譜,要么是完全實現成 Unix 下的 Cron 程序,自帶不能去掉的命令執行功能,然而 ScopeDB 只需要能解析 Cron 表達式即可。
因此,考慮到解析 Cron 表達式并不復雜,且即使使用 croner 或 saffron 也需要對其進行一些修改以滿足 ScopeDB 的需求,我決定自己實現一個 Cron 表達式解析庫。
Cronexpr 由此誕生。它的原型接口非常簡單:
let?crontab = cronexpr::parse_crontab("2 4 * * * Asia/Shanghai").unwrap();// case 0. match timestamp
assert!(crontab.matches("2024-09-24T04:02:00+08:00").unwrap());
assert!(!crontab.matches("2024-09-24T04:01:00+08:00").unwrap());// case 1. find next timestamp with timezone
assert_eq!(crontab.find_next("2024-09-24T10:06:52+08:00").unwrap().to_string(),"2024-09-25T04:02:00+08:00[Asia/Shanghai]"
);
實現的細節就不展開了,這里討論一下對依賴的選用。
Cronexpr 的依賴只有兩個,一個是上面提到的 jiff 庫,用來處理時間戳相關的邏輯,另一個是 winnow 庫,用來處理 Cron 表達式的解析。
選用 jiff 的道理非常簡單,它是目前最可靠的 Rust 日期時間庫,對時區、夏令時、各種日期計算都有很好的支持。歷法的制定實際上是一種話語權的爭奪,歷法與日期變更的計算充滿了人類世界的不靠譜特質,能把里面各種爛坑填好的庫不可多得。jiff 庫的作者有長期良好的聲譽,他是 Golang 生態當中 toml 庫的作者,也是 Rust Team 的官方成員,是 Rust Regex 庫的作者,也還創作過 ripgrep 和 csv 等高質量開源庫。
選用 winnow 的理由就相對隨機。其實 Rust 生態里比較有名的 Parser Combinator 是 nom 庫。但是當時 ScopeDB 已經用過 nom 庫來解析 ScopeQL 了,我感覺使用的過程中有一些不爽的點。正好當時 nom 一年多沒發新版本了,我想著 winnow 號稱是它的積極維護的分支,維護者是 Rust Team 的活躍成員,或許可以試試。實際情況是也沒有好到哪里去,而且我寫的時候不是完全按組合子的味道寫的,最終結果有點半手寫半組合子的風格,可能還不如純手寫。不過好在 Cron 表達式的結構非常固定,而且可見的迭代需求不多,大致知道怎么回事,代碼一直能看懂就差不多了。客觀來說,最初采用 winnow 還是節省了不少 while-if-match 式的樣板代碼。
文檔與測試
由于 Cronexpr 的邏輯相對簡單,經歷過大約兩周的迭代后,所有接口和實現基本就已經穩定了。這期間主要修正接口和實現的反饋,來自于 ScopeDB 的實用情況,以及參考現有軟件庫的接口設計方式。例如,我就是在看到 croner 和 saffron 的接口設計后,才想到可以做一個?iter_after
?的接口。
同時,由于 Cron 表達式有參考實現,而且在開發的過程中,我遇到了很多具體“村規”的理解和處理,疊加上當時受到 jiff 庫詳實文檔的啟發,我把 Cronexpr 開發過程當中所有的設計理念跟概念都用文檔的形式記錄了下來。后來發布到 Hacker News 上的時候,也有讀者回應稱從文檔里了解到許多此前不知道的 Cron 表達式的細節。
除此以外,實現一個成熟的功能,也很容易找到前人寫過的測試集。Cronexpr 最適合的測試模式,當然是所謂的快照測試(Snapshot Testing),即把解析和匹配 Cron 表達式的返回值作為快照記錄下來,在迭代過程中保證這些返回值總是一致。因為返回值的文本,尤其是報錯時的文本經常相對冗長,所以用手寫?assert_eq!
?的方式可能會難以維護。我用?insta[5]?工具維護了近一百個測試結果的快照。
順帶一提,ScopeDB 也重度使用 insta 做快照測試,包括 ScopeQL 的 Parsing 測試,以及端到端的類 sqllogictest 的測試。實際效果跟 sqllogictest 基本一樣,而且能夠跟 Rust 原生的功能做更緊密且可定制化的集成,不用依賴一個新的 DSL 跟每次需要新功能就要對 DSL 做擴展開發。
實際效果與現狀
ScopeDB 從第一個測試版本開始就支持?CREATE JOB
?功能。
Cronexpr 首先被用在?CREATE JOB
?的執行上,即在創建后臺作業時先校驗 Cron 表達式是否是合法的。隨后,ScopeDB 的 server 進程本身會啟動一個線程,實時監督哪些 JOB 已經到了需要再次調度的時刻,并觸發 JOB 執行。雖然涉及的代碼行數不多,但卻是 ScopeDB 能夠高效運維的核心能力之一。
目前,Cronexpr 已經發布了 1.0 版本,除了偶爾跟進一下依賴庫的版本,平常基本沒有什么需要再做開發的地方。我能想到的迭代需求,可能還有以下幾個:
有開發者提出過給 Cronexpr 做一套 Fuzz 測試。我覺得沒啥必要,因為實際的 case 非常有限,目前基本都枚舉完了。但是如果有人做了一個不錯的 Fuzz 方案,可以拿 Cronexpr 做實驗。
把 winnow 的依賴去掉。這樣,繼我在寫作本文之前把 thiserror 依賴去掉以后,Cronexpr 就可以僅依賴不可能去掉的 jiff 庫,盡可能的減少不必要的依賴。如前所述,winnow 在 Cron 表達式這樣一個語法非常固定的場景里作用有限。
有人可能想要支持可選的秒級、年份級 Cron 表達式。不過這些都是比較小眾的需求,我在自己有實際需求之前就不實現了。
涓滴開源與開源的可持續性
在 ScopeDB 的開發過程中,不只有 Cronexpr 一個開源庫誕生。上面提到的 server 進程本身會啟動一系列后臺線程,這些后臺線程的調度就是由?Fastimer[6]?庫支持的。此外,ScopeDB 使用的?Fastrace[7]?+?Logforth[8]?方案,實際上是當前 Rust 生態非常先進且可靠的一套日志追蹤方案。這些案例或許我在后續文章中還會展開。
要說跟 Cronexpr 相似的,應當是?StackSafe[9]?這個用于避免遞歸調用和遞歸數據結構棧溢出的小公共庫。庫作者 Andy 老師還寫了一篇博文[10]介紹其設計理念跟使用方式。
其他還有一些體量不大的軟件庫,比如 Mea 和 Fastpool 等等。但是它們設計到我很想吐槽的 Async Rust 生態,所以可能會單獨寫幾篇文章討論它們的情況。
回到開源項目的可持續性上來,這些項目的源頭活水,首先就是被 ScopeDB 商業產品所需求。在開源以后,例如 Cronexpr、Fastrace 和 Logforth 等軟件庫,也逐漸有了第三方的下游依賴。這些新的用戶為我們開源的項目提供了寶貴的反饋,有些幫我們提前解決了將要遇到的問題,有些幫助我們更好的設計接口。用戶反饋本身也是對工程師編寫軟件的正向反饋:看到自己寫作的軟件能夠幫到更多的人,被更多的人認可,是一件非常開心的事情。
因此,涓滴開源是一種可持續的開源模式。開源開發者通過商業軟件已經實現了經濟可持續,將開發商業軟件過程中,微小但完整的功能模塊,且其本身不產生商業價值,反而最好開源尋求同行評審,以公共庫的形式開源發布出來,這是商業開源生產軟件的其中一種形式。
這個世界上這樣產生的開源軟件有很多,包括前文提到的 Cloudflare 開源的 saffron 也屬于此類。只要公司不做禁止,這類軟件就可自由生長;而如果企業能夠稍加引導,這些軟件就能成為技術品牌影響力的一部分,其價值將在長時間跨度上不斷為企業帶來正面的回報。
參考資料
[1]?
Cronexpr:?https://crates.io/crates/cronexpr
[2]?ScopeDB 云數據庫:?https://www.scopedb.io/
[3]?croner:?https://docs.rs/croner/latest/croner/
[4]?saffron:?https://docs.rs/saffron/latest/saffron/
[5]?insta:?https://insta.rs/
[6]?Fastimer:?https://github.com/fast/fastimer
[7]?Fastrace:?https://github.com/fast/fastrace
[8]?Logforth:?https://github.com/fast/logforth
[9]?StackSafe:?https://github.com/fast/stacksafe
[10]?博文:?https://fast.github.io/blog/stacksafe-taming-recursion-in-rust-without-stack-overflow/