使用 MoonBit 在Golem Cloud上开发应用程序
副标题:MoonBit: First Look at New Language Through Building WASM Back End
介绍
Golem 架构
-
列表
-
归档
-
邮件通知
初步 MoonBit 实现
moon new
创建一个新的 lib 项目。这将创建一个包含单个包的新项目。为了匹配我们的架构,我们将开始创建多个包,每个包对应一个要开发的组件(列表、归档、邮件)。
moon.pkg.json
文件:
{"import": []}
列表模型
struct Document { mut items: Array[String] }
Document
实现方法,支持我们希望的文档编辑操作。在此级别,我们不关心协作编辑或连接的用户,只需将文档建模为纯数据结构:
///| 创建一个空的文档
pub fn Document::new() -> Document {
{ items: [] }
}
///| 向文档添加一个新项
pub fn add(self : Document, item : String) -> Unit {if self.items.search(item).is_empty() {self.items.push(item)
}
}
///| 从文档中删除一个项
pub fn delete(self : Document, item : String) -> Unit {self.items = self.items.filter(fn(i) { item != i })
}
///| 向文档中插入一个项,如果 after 不在文档中,则新项插入到末尾。
pub fn insert(self : Document, after~ : String, value~ : String) -> Unit {let index = self.items.search(after)match index {Some(index) => self.items.insert(index + 1, value)None => self.add(value)
}
}
///| 获取文档的项视图
pub fn get(self : Document) -> ArrayView[String] {self.items[:]
}
///| 遍历文档中的项
pub fn iter(self : Document) -> Iter[String] {self.items.iter()
}
test "new document is empty" {let empty = Document::new()assert_eq!(empty.items, []) }
inspect
函数,测试可以使用快照值来进行比较。 moon CLI
工具和 IDE 集成提供了在需要时自动更新这些测试函数中的快照值( content=
部分)的方法:
test "basic document operations" {let doc = Document::new()
..add("x")
..add("y")
..add("z")
..insert(after="y", value="w")
..insert(after="a", value="b")
..delete("z")
..delete("f")
inspect!(
doc.get(),
content=
#|["x", "y", "w", "b"]
,
)
}
列表编辑器状态
Document
类型的编辑器状态管理。提醒一下,我们决定每个列表组件的实例(Golem 工作者)只负责编辑一个单一的列表。因此,我们不需要关心存储和索引列表,或将连接路由到对应的节点,Golem 会自动管理这一切。
///| 文档状态struct State {
document : Document
connected : Map[ConnectionId, EditorState]mut last_connection_id : ConnectionIdmut archived : Boolmut email_deadline : @datetime.DateTimemut email_recipients : Array[EmailAddress]
}
-
连接的编辑者的映射,以及与每个编辑者相关的状态
-
用于生成新连接 ID 的上一个连接 ID
-
文档是否已归档
-
何时发送电子邮件通知,发送给哪些收件人
Document
类型,因此让我们继续定义 State
字段中使用的其他类型。
ConnectionId
将是一个包装整数的新类型:
///| 连接的编辑者的标识符type ConnectionId Int derive(Eq, Hash)
///| 生成下一个唯一的连接 IDfn next(self : ConnectionId) -> ConnectionId {ConnectionId(self._ + 1)
}
Map
的键,因此我们需要为它实现 Eq
和 Hash
类型类。MoonBit 可以自动为新类型派生这些类型类。除此之外,我们还定义了一个名为 next
的方法,用于生成一个递增的连接 ID。
EditorState
结构存储每个连接的编辑者的信息。为了简化起见,我们只存储编辑者的电子邮件地址和自上次轮询以来的更改事件缓冲区。
String
类型:
///| 连接编辑者的电子邮件地址type EmailAddress String
Change
枚举描述了对文档所做的可能更改:
///| 编辑文档时的可观察更改enum Change {Added(String)Deleted(String)Inserted(after~ : String, value~ : String) } derive(Show)
Show
(或手动实现),可以使用 inspect
测试函数比较更改数组的字符串快照与 poll
函数的结果。
EditorState
:
///| 每个连接的编辑者的状态struct EditorState { email : EmailAddressmut events : Array[Change] }
email
字段在连接的编辑者中始终不变,但 events
数组会随着每次调用 poll
被重置,以便下次轮询时仅返回新的更改。为了实现这一点,我们必须将其标记为可变( mut
)。
State
引入的最后一个新类型是表示时间点的东西。MoonBit 的核心标准库目前没有这个功能,但已经有一个名为 mooncakes
的包数据库,里面发布了 MoonBit 的包。在这里我们可以找到一个叫 datetime
的包。通过使用 moon CLI 可以将其添加到项目中:
moon add suiyunonghen/datetime
moon.pkg.json
将其导入:
{"import": ["suiyunonghen/datetime"]}
@datetime.DateTime
引用该包中的 DateTime
类型。
State
的方法之前,我们还需要考虑错误处理—— State
上的某些操作可能会失败,例如如果使用了错误的连接 ID,或者如果文档已归档时仍进行编辑操作。MoonBit 内建了错误处理支持,首先通过以下方式定义我们自己的错误类型:
///| 编辑器状态操作的错误类型
type! EditorError {///| 当使用无效的连接 ID 时返回的错误InvalidConnection(ConnectionId)///| 尝试修改已归档的文档时的错误
AlreadyArchived
}
State
的所有方法,但完整的源代码可以在 GitHub 上找到。
connect
方法将新的连接 ID 与连接的用户关联,并返回当前文档状态。这对于使用 poll
的结果非常重要——返回的更改列表必须精确应用到客户端上的该文档状态。
///| 连接一个新的编辑者pub fn connect(self : State,
email : EmailAddress
) -> (ConnectionId, ArrayView[String]) {let connection_id = self.last_connection_id.next()self.last_connection_id = connection_idself.connected.set(connection_id, EditorState::new(email))
(connection_id, self.document.get())
}
Document
定义的编辑操作的基础上,但除了这些,它们还执行以下任务:
-
验证连接 ID
-
验证文档是否尚未归档
-
向每个连接编辑者的状态添加一个
Change
事件 -
更新
email_deadline
和email_recipients
字段,因为每次编辑操作都会重置发送电子邮件的超时时间
///| 如果文档已归档则失败fn ensure_not_archived(self : State) -> Unit!EditorError {
guard not(self.archived) else { raise AlreadyArchived }
}
///| 如果给定的 connection_id 不在连接映射中,则失败fn ensure_is_connected(self : State,
connection_id : ConnectionId
) -> Unit!EditorError {
guard self.connected.contains(connection_id) else {
raise InvalidConnection(connection_id)
}
}
Unit!EditorError
返回类型表示这些方法可能会失败,并返回 EditorError
。
///| 向每个连接编辑者的状态添加更改事件fn add_event(self : State, change : Change) -> Unit {for editor_state in self.connected.values() {
editor_state.events.push(change)
}
}
///| 在更新后更新 email_deadline 和 email_recipients 字段fn update_email_properties(self : State) -> Unit {let now = @datetime.DateTime::from_unix_mseconds(0) // TODOlet send_at = now.inc_hour(12)let email_list = self.connected_editors()self.email_deadline = send_atself.email_recipients = email_list
}
datetime
库没有获取当前日期和时间的功能,我们需要这个功能来使此函数正常工作。我们将在针对 WebAssembly(和 Golem)时解决这个问题,因为获取当前系统时间依赖于目标平台。
add
,是直截了当的:
///| 作为连接的编辑者向文档添加新元素pub fn add(self : State,
connection_id : ConnectionId,
value : String
) -> Unit!EditorError {self.ensure_not_archived!()self.ensure_is_connected!(connection_id)self.document.add(value)self.add_event(Change::Added(value))self.update_email_properties()
}
poll
也很简单,因为我们已经为每个连接维护了更改列表,我们只需要在每次调用后重置它:
///| 返回自上次调用 poll 以来发生的更改列表pub fn poll(self : State,
connection_id : ConnectionId
) -> Array[Change]!EditorError {match self.connected.get(connection_id) {Some(editor_state) => {let events = editor_state.events
editor_state.events = []
events
}None => raise InvalidConnection(connection_id)
}
}
列表归档
Document
类型,因为它表示的是一个可编辑的文档。相反,我们在归档包中定义了一些新类型:
///| 文档的唯一名称type DocumentName String derive(Eq, Hash)
///| DocumentName 的 Show 实现impl Show for DocumentName with output(self, logger) { self._.output(logger) }
///| 一个单一的已归档不可变文档,封装了文档的名称及其项struct ArchivedDocument {
name : DocumentName
items : Array[String]
} derive(Show)
///| 归档是一个已归档文档的列表struct Archive {
documents : Map[DocumentName, ArchivedDocument]
}
insert
方法和一个迭代所有已归档文档的方法:
///| 归档一个命名文档pub fn insert(self : Archive,
name : DocumentName,
items : Array[String]
) -> Unit {self.documents.set(name, { name, items })
}
///| 迭代所有已归档文档pub fn iter(self : Archive) -> Iter[ArchivedDocument] {self.documents.values()
}
Archive
实例来模拟这一点:
pub let archive: Archive = Archive::new()
State::archive
方法中调用它:
pub fn archive(self : State) -> Unit {self.archived = truelet name = @archive.DocumentName("TODO")
@archive.archive.insert(name, self.document.iter().to_array())
}
State
中没有存储文档的名称——我们并没有把它存储在任何地方。这是故意的,正如我们之前讨论的那样,工人的名称将作为文档的唯一标识符。一旦我们进入 Golem 特定的实现阶段,获取工人的名称将以 Golem 特有的方式完成。
发送一封电子邮件
State
类型中准备好了部分电子邮件发送逻辑:它有一个截止日期和一份收件人名单。我们的想法是,当创建一个新的列表时,我们会启动一个电子邮件发送工人,并让它与我们的编辑会话并行运行,形成一个循环。在这个循环中,首先查询列表编辑状态中的截止日期和收件人名单,然后它会一直休眠直到指定的截止日期。当它醒来时(12 小时后),它再次查询列表,如果已经过了截止日期,说明在此期间没有进一步的编辑操作。然后,它会向收件人列表发送通知邮件。
编译为 Golem 组件
绑定
wit-bindgen 工具
已经支持 MoonBit,因此我们可以首先安装最新版本:
cargo install wit-bindgen-cli
wit-bindgen
,但该版本还不支持 MoonBit。新版本应该可以很好地工作,但 Golem 的示例代码并没有在这个版本上进行过测试。
package demo:lst;
interface api {
record connection {
id: u64
}
record insert-params {
after: string,
value: string
}
variant change {added(string),deleted(string),inserted(insert-params)
}
add: func(c: connection, value: string) -> result<_, string>;
delete: func(c: connection, value: string) -> result<_, string>;
insert: func(c: connection, after: string, value: string) -> result<_, string>;
get: func() -> list<string>;
poll: func(c: connection) -> result<list<change>, string>;
connect: func(email: string) -> tuple<connection, list<string>>;
disconnect: func(c: connection) -> result<_, string>;
connected-editors: func() -> list<string>;
archive: func();
is-archived: func() -> bool;
}
interface email-query {
deadline: func() -> option<u64>;
recipients: func() -> list<string>;
}
world lst {// .. imports to be explained later ..
export api;
export email-query;
}
State
类型实现的方法。另一个是一个内部 API,用于电子邮件组件查询截止日期和收件人,如前所述。
package demo:archive;
interface api {
record archived-list {
name: string,
items: list<string>
}
store: func(name: string, items: list<string>);
get-all: func() -> list<archived-list>;
}
world archive {// .. 导入稍后会解释 ..
export api;
}
package demo:email;
interface api {use golem:rpc/types@0.1.0.{uri};
send-email: func(list-uri: uri);
}
world email {// .. 导入稍后会解释 ..
export api;
}
uri
。这是必要的,因为电子邮件工人需要调用它所从中生成的特定列表工人。具体细节稍后会解释。
moon.mod.json
标识),并仅将 list
、 email
和 archive
创建为内部包。此时,我们需要做一些更改,因为我们需要为每个我们想要编译成独立 Golem 组件的代码块创建一个单独的模块。通过在每个子目录中运行 wit-bindgen
(如下所示),它实际上会为我们生成模块定义。
src/archive
移到 archive
等,并将之前编写的源代码移到 archive/src
。这样生成的绑定和我们手写的实现将并排放置。我们还可以删除顶级模块定义的 JSON 文件。
wit-bindgen moonbit wit
stub.wit
文件,再次运行此命令将会覆盖我们的更改。为避免这种情况,可以使用以下方式运行:
wit-bindgen moonbit wit --ignore-stub
moon build --target wasm
./target/wasm/release/build/gen/gen.wasm
的 WASM 模块。这还不是一个 WASM 组件——因此不能直接在 Golem 中使用。为了实现这一点,我们需要使用另一个命令行工具 wasm-tools
,将该模块转换为一个自描述其高级导出接口的组件。
WIT 依赖项
wit-deps
工具。
wit-deps
:
cargo install wit-deps-cli
wit
目录中创建一个 deps.toml
文件,内容如下:
all = "https://github.com/golemcloud/golem-wit/archive/main.tar.gz"
wit/deps
目录:
wit-deps update
实现导出功能
archive/gen/interface/demo/archive/api/stub.mbt
位置生成了一个 stub.mbt
文件,其中包含两个需要实现的导出函数。我们在使用代码生成器时通常会遇到一个问题:我们在 WIT 中定义了 archived-list
,并且绑定生成器根据它生成了以下 MoonBit 定义:
// Generated by wit-bindgen
0.36.0. DO NOT EDIT!pub struct ArchivedList { name : String; items : Array[String] } derive()
ArchivedDocument
!唯一的区别是使用了 DocumentName
新类型,并且我们的版本派生了一个 Show
实例。我们可以决定放弃使用这个新类型,并在我们的业务逻辑中使用生成的类型,或者我们可以保持生成的类型与我们的实际代码分离。(这其实并不特定于 MoonBit 或 WASM 工具链,在任何基于代码生成器的方法中都会遇到这个问题。)
stub.mbt
文件。
store
。我们可以通过调用 insert
在我们的单例顶级 Archive
中实现它,就像我们之前直接将归档包连接到列表包时做的那样:
pub fn store(name : String, items : Array[String]) -> Unit { @src.archive.insert(@src.DocumentName(name), items) }
stub
包的 JSON 中导入我们的主归档源:
{"import": [{ "path" : "demo/archive/ffi", "alias" : "ffi" },{ "path" : "demo/archive/src", "alias" : "src" }]}
pub fn get_all() -> Array[ArchivedList] { @src.archive .iter() .map(fn(archived) { { name: archived.name._, items: archived.items } }) .to_array() }
ArchivedDocument
结构体设置为 pub
,否则我们无法从 stub
包访问它的 name
和 items
字段。
list/gen/interface/demo/lst/api/stub.mbt
和 list/gen/interface/demo/lst/emailQuery/stub.mbt
中),使用我们现有的 State
实现。
EditorError
的失败映射到 WIT 定义中使用的字符串错误。首先我们为 EditorError
定义一个 to_string
方法:
pub fn to_string(self : EditorError) -> String {match self {InvalidConnection(id) => "Invalid connection ID: \{id._}", AlreadyArchived => "Document is already archived" } }
?
和 map_err
:
pub fn add(c : Connection, value : String) -> Result[Unit, String] { @src.state .add?(to_connection_id(c), value) .map_err(fn(err) { err.to_string() }) }
使用宿主函数
update_email_properties
函数时,我们无法正确查询当前时间来计算适当的截止日期。现在我们正在面向 Golem,我们可以使用 WebAssembly 系统接口(WASI)来访问诸如系统时间之类的功能。一种方法是使用已发布的 wasi-bindings 包
,但既然我们已经从 WIT 生成绑定,我们可以直接使用我们自己生成的绑定来导入宿主函数。
world lst { export api; export email-query; import wasi:clocks/wall-clock@0.2.0; }
--ignore-stub
,以避免覆盖我们的存根实现!),并将其导入到我们的主包( src
)中:
{"import": ["suiyunonghen/datetime",{ "path" : "demo/lst/interface/wasi/clocks/wallClock", "alias" : "wallClock" }]}
now
函数来查询当前的系统时间,并将其转换为我们之前使用的 datetime
模块的 DateTime
类型:
///| Queries the WASI wall clock and returns it as a @datetime.DateTime////// Note that DateTime has only millisecond precisionfn now() -> @datetime.DateTime {let wasi_now = @wallClock.now()let base_ms = wasi_now.seconds.reinterpret_as_int64() * 1000;let nano_ms = (wasi_now.nanoseconds.reinterpret_as_int() / 1000000).to_int64();
@datetime.DateTime::from_unix_mseconds(base_ms + nano_ms)
}
Golem 应用清单
golem-cli
生成必要的文件来进行工作者之间的通信,并且也更容易将编译后的组件部署到 Golem。
构建步骤
-
(可选)使用
wit-bindgen ... --ignore-stub
重新生成 WIT 绑定。 -
使用
moon build --target wasm
将 MoonBit 源代码编译为 WASM 模块。 -
使用
wasm-tools component embed
将 WIT 规范嵌入到自定义的 WASM 部分中。 -
使用
wasm-tools component new
将 WASM 模块转换为 WASM 组件。
清单模板
golem.yaml
。接下来,我们设置一个临时目录和一个共享目录,用于存放我们之前通过 wit-deps
获取的 WIT 依赖:
# IDEA 的架构:
# $schema: https://schema.golem.cloud/app/golem/1.1.0/golem.schema.json
# vscode-yaml 的架构
# yaml-language-server: $schema=https://schema.golem.cloud/app/golem/1.1.0/golem.schema.json
tempDir: target/golem-temp
witDeps:
- common-wit/deps
deps.toml
移动到 common-wit
目录,并在根目录执行 wit-deps update
,我们可以将所有需要的 WASI 和 Golem API 填充到这个依赖目录中。
templates:moonbit:profiles:release:sourceWit: witgeneratedWit: wit-generatedcomponentWasm: ../target/release/{{ componentName }}.wasmlinkedWasm: ../target/release/{{ componentName }}-linked.wasm
target/release
目录下。
build:- command: wit-bindgen moonbit wit-generated --ignore-stub --derive-error --derive-showsources:- wit-generatedtargets:- ffi- interface- world- command: moon build --target wasm- command: wasm-tools component embed wit-generated target/wasm/release/build/gen/gen.wasm -o ../target/release/{{ componentName }}.module.wasm --encoding utf16mkdirs:- ../target/release- command: wasm-tools component new ../target/release/{{ componentName }}.module.wasm -o ../target/release/{{ componentName }}.wasm
golem app clean
命令中被清理,还可以定义自定义命令供 golem app xxx
执行:
clean:- target- wit-generatedcustomCommands:update-deps:- command: wit-deps updatedir: ..regenerate-stubs:- command: wit-bindgen moonbit wit-generated
golem.yaml
来将新的 MoonBit 模块添加到这个 Golem 项目中——比如 archive/golem.yaml
和 list/golem.yaml
。
# IDEA 的架构:
# $schema: https://schema.golem.cloud/app/golem/1.1.0/golem.schema.json
# vscode-yaml 的架构
# yaml-language-server: $schema=https://schema.golem.cloud/app/golem/1.1.0/golem.schema.json
components:
archive:
template: moonbit
构建组件
golem app build
golem app build
会对 WIT 定义进行一些转换。这意味着我们之前编写的存根文件位置已经不正确。修复这个问题最简单的方法是删除所有由 wit-bindgen
生成的目录(但首先要备份手写的存根文件!),然后将存根文件复制回新创建的目录中。我们在这里不会进一步讨论这个问题。本文的博客会逐步介绍如何使用 MoonBit 构建 Golem 应用,并且在后期介绍应用清单,但推荐的方法是从一开始就使用应用清单,这样就无需做这些修复了。
初次尝试
golem start -vv
$ golem component add --component-name archive
Added new component archive
Component URN: urn:component:bde2da89-75a8-4adf-953f-33b360c978d0
Component name: archive
Component version: 0
Component size: 9.35 KiB
Created at: 2025-01-03 15:09:05.166785 UTC
Exports:
demo:archive-interface/api.{get-all}() -> list<record { name: string, items: list<string> }>
demo:archive-interface/api.{store}(name: string, items: list<string>)
$ golem component add --component-name list
Added new component list
Component URN: urn:component:b6420554-62b5-4902-8994-89c692a937f7
Component name: list
Component version: 0
Component size: 28.46 KiB
Created at: 2025-01-03 15:09:09.743733 UTC
Exports:
demo:lst-interface/api.{add}(c: record { id: u64 }, value: string) -> result<_, string>
demo:lst-interface/api.{archive}()
demo:lst-interface/api.{connect}(email: string) -> tuple<record { id: u64 }, list<string>>
demo:lst-interface/api.{connected-editors}() -> list<string>
demo:lst-interface/api.{delete}(c: record { id: u64 }, value: string) -> result<_, string>
demo:lst-interface/api.{disconnect}(c: record { id: u64 }) -> result<_, string>
demo:lst-interface/api.{get}() -> list<string>
demo:lst-interface/api.{insert}(c: record { id: u64 }, after: string, value: string) -> result<_, string>
demo:lst-interface/api.{is-archived}() -> bool
demo:lst-interface/api.{poll}(c: record { id: u64 }) -> result<list<variant { added(string), deleted(string), inserted(record { after: string, value: string }) }>, string>
demo:lst-interface/email-query.{deadline}() -> option<u64>
demo:lst-interface/email-query.{recipients}() -> list<string>
store
函数,然后调用 get-all
函数,使用 CLI 的 worker invoke-and-await
命令来尝试归档组件:
-
$ golem worker invoke-and-await --worker urn:worker:bde2da89-75a8-4adf-953f-33b360c978d0/archive --function 'demo:archive-interface/api.{store}' --arg '"list1"' --arg '["x", "y", "z"]' Empty result.
-
$ golem worker invoke-and-await --worker urn:worker:bde2da89-75a8-4adf-953f-33b360c978d0/archive --function 'demo:archive-interface/api.{get-all}' Invocation results in WAVE format: '[{name: "list1", items: ["x", "y", "z"]}]'
store
函数,然后调用 get-all
函数,使用 CLI 的 worker invoke-and-await
命令来尝试归档组件:
--build-profile debug
,我们还会看到一个漂亮的调用栈):
Failed to create worker b6420554-62b5-4902-8994-89c692a937f7/list6: Failed to instantiate worker -1/b6420554-62b5-4902-8994-89c692a937f7/list6: error while executing at wasm backtrace:
0: 0x19526 - wit-component:shim!indirect-wasi:clocks/wall-clock@0.2.0-now
1: 0x414b - <unknown>!demo/lst/interface/wasi/clocks/wallClock.wasmImportNow
2: 0x4165 - <unknown>!demo/lst/interface/wasi/clocks/wallClock.now
3: 0x42c1 - <unknown>!demo/lst/src.now
4: 0x433d - <unknown>!@demo/lst/src.State::update_email_properties
5: 0x440e - <unknown>!@demo/lst/src.State::new
6: 0x5d81 - <unknown>!*init*/38
State
,并且在它的构造函数中尝试调用一个 WASI 函数(获取当前的日期和时间)。这个时机太早了;所以我们需要修改 State::new
方法,避免在初始化时调用任何宿主函数:
///| 创建一个新的空文档编辑状态pub fn State::new() -> State {let state = {
document: Document::new(),
connected: Map::new(),
last_connection_id: ConnectionId(0),
archived: false,
email_deadline: @datetime.DateTime::from_unix_mseconds(0), // 注意:不能在这里使用 now(),因为它会在初始化时运行(由于全局 state 变量)
email_recipients: [],
}
state
}
Worker 到 Worker 通信
列表调用归档
archive()
时,它需要在一个单例的归档 worker 中调用 store
函数,并将数据发送过去。
components:list:template: moonbitdependencies:list:- type: wasm-rpctarget: archive
golem app build
会进行许多新的构建步骤——包括生成和编译一些 Rust 源代码,这些代码在 Golem 的下一个版本中将不再需要。
{"import": ["suiyunonghen/datetime",{ "path" : "demo/lst/interface/wasi/clocks/wallClock", "alias" : "wallClock" },{ "path" : "demo/lst/interface/demo/archive_stub/stubArchive", "alias": "stubArchive" },{ "path" : "demo/lst/interface/golem/rpc/types", "alias": "rpcTypes" }]}
let archive_component_id = "bde2da89-75a8-4adf-953f-33b360c978d0"; // TODOlet archive = @stubArchive.Api::api({ value: "urn:worker:\{archive_component_id}/archive"});let name = "TODO"; // TODO
archive.blocking_store(name, self.document.iter().to_array());
store
函数。
-
我们不应该硬编码归档组件的 ID,因为它是在组件首次上传到 Golem 时自动生成的;
-
我们需要知道我们自己的 worker 名称,它将作为列表的名称。
GOLEM_WORKER_NAME
环境变量设置为 worker 的名称,我们也可以通过自定义环境变量手动提供值给 worker。这使得我们可以从外部注入组件 ID(直到 Golem 1.2 版本添加了更复杂的配置功能)。
import wasi:cli/environment@0.2.0;
golem app build
以重新生成绑定,并在 list/src
的 MoonBit 包中导入它:
{ "path" : "demo/lst/interface/wasi/cli/environment", "alias": "environment" }
///| 使用 WASI 获取环境变量fn get_env(key : String) -> String? {
@environment.get_environment()
.iter()
.find_first(fn(pair) {
pair.0 == key
})
.map(fn(pair) {
pair.1
})
}
let archive_component_id = get_env("ARCHIVE_COMPONENT_ID").or("unknown");
// ...let name = get_env("GOLEM_WORKER_NAME").or("unknown");
ARCHIVE_COMPONENT_ID
:
golem worker start --component urn:component:b6420554-62b5-4902-8994-89c692a937f7 --worker-name list10 --env "ARCHIVE_COMPONENT_ID=bde2da89-75a8-4adf-953f-33b360c978d0"
get-all
——我们可以看到远程过程调用是有效的!
列表与电子邮件组件通信
wasm-rpc
),同时电子邮件也依赖于列表(同样通过 wasm-rpc
)。我们需要在两个方向上进行通信。
subscribe-instant
函数。
send-email
函数的 MoonBit 实现(我们已在 email.wit
文件中定义该函数):
///| 存储电子邮件发送者的配置信息pub(all) struct Email {
list_worker_urn : String
}
///| 执行发送电子邮件的循环pub fn run(self : Email) -> Unit {while true {match self.get_deadline() {Some(epoch_ms) => {let now = @wallClock.now()let now_ms = now.seconds * 1000 +
(now.nanoseconds.reinterpret_as_int() / 1000000).to_uint64()let duration_ms = epoch_ms.reinterpret_as_int64() -
now_ms.reinterpret_as_int64()if duration_ms > 0 {sleep(duration_ms.reinterpret_as_uint64())
} else {send_emails(self.get_recipients())
}continue
}None => break
}
}
}
wallClock
接口来查询当前时间,并根据从相关列表 worker 获取的截止日期计算等待的时长。 get_deadline
和 get_recipients
方法则是利用 Golem 的 worker-to-worker 通信进行的。
///| 获取与列表 worker 关联的当前截止日期fn get_deadline(self : Email) -> UInt64? {let api = @stubLst.EmailQuery::email_query({ value: self.list_worker_urn })
api.blocking_deadline()
}
///| 获取与列表 worker 关联的当前收件人列表fn get_recipients(self : Email) -> Array[String] {let api = @stubLst.EmailQuery::email_query({ value: self.list_worker_urn })
api.blocking_recipients()
}
1. 休眠功能
subscribe-duration
函数来获取一个可轮询对象,并在该对象上进行轮询,从而实现休眠。由于我们只传递了一个单一的轮询对象给列表,它将在目标截止日期到达时返回:
///| 休眠指定的毫秒数fn sleep(ms : UInt64) -> Unit {let ns = ms * 1000000let pollable = @monotonicClock.subscribe_duration(ns)let _ = @poll.poll([pollable])
}
2. 列表中的非阻塞调用
if not(self.email_worker_started) {let email_component_id = get_env("EMAIL_COMPONENT_ID").or("unknown");let name = get_env("GOLEM_WORKER_NAME").or("unknown")let self_component_id = get_env("GOLEM_COMPONENT_ID").or("unknown")let api = @stubEmail.Api::api({ value: "urn:worker:\{email_component_id}:\{name}"} )
api.send_email({ value: "urn:worker:\{self_component_id}:\{name}"} )self.email_worker_started = true;
}
发送电子邮件
https://api.sendgrid.com/v3/mail/send
发送一个 HTTP POST 请求,并附上已配置的授权头和描述电子邮件发送请求的 JSON 正文。
const AUTHORITY : String = "api.sendgrid.com"const PATH : String = "/v3/mail/send"
type! HttpClientError String
///| 如果字符串中包含非 ASCII 字符,则将其转换为 ASCII 字节数组,否则失败fn string_to_ascii(
what : String,
value : String
) -> FixedArray[Byte]!HttpClientError {let result = FixedArray::makei(value.length(), fn(_) { b' ' })for i, ch in value {if ch.to_int() < 256 {
result[i] = ch.to_int().to_byte()
} else {
raise HttpClientError("The \{what} contains non-ASCII characters")
}
}
result
}
///| 构建 SendGrid 发送邮件的 JSON 正文,并将其转换为 ASCII 字节数组fn payload(recipients : Array[String]) -> FixedArray[Byte]!HttpClientError {let email_addresses = recipients
.iter()
.map(fn(email) { { "email": email, "name": email } })
.to_array()
.to_json()let from : Json = { "email": "demo@vigoo.dev", "name": "Daniel Vigovszky" }let json : Json = {"personalizations": [{ "to": email_addresses, "cc": [], "bcc": [] }],"from": from,"subject": "Collaborative list editor warning","content": [
{"type": "text/html","value": "<p>The list opened for editing has not been changed in the last 12 hours</p>",
},
],
}let json_str = json.to_string()
string_to_ascii!("constructed JSON body", json_str)
}
///| 获取 SENDGRID_API_KEY 环境变量的值,并转换为 ASCII 字节数组fn authorization_header() -> FixedArray[Byte]!HttpClientError {let key_str = @environment.get_environment()
.iter()
.find_first(fn(pair) { pair.0 == "SENDGRID_API_KEY" })
.map(fn(pair) { pair.1 })
.unwrap()
string_to_ascii!("provided authorization header via SENDGRID_API_KEY", key_str,
)
}
Result
类型的返回值,因此我们的代码将会比较冗长:
let headers = @httpTypes.Fields::fields()
headers
.append("Authorization", authorization_header!())
.map_err(fn(error) {HttpClientError("设置 Authorization 头部失败: \{error}")
})
.unwrap_or_error!()
let request = @httpTypes.OutgoingRequest::outgoing_request(headers)
request
.set_authority(Some(AUTHORITY))
.map_err(fn(_) { HttpClientError("设置请求 authority 失败") })
.unwrap_or_error!()
request
.set_method(@httpTypes.Method::Post)
.map_err(fn(_) { HttpClientError("设置请求方法失败") })
.unwrap_or_error!()
request
.set_path_with_query(Some(PATH))
.map_err(fn(_) { HttpClientError("设置请求路径失败") })
.unwrap_or_error!()
request
.set_scheme(Some(@httpTypes.Scheme::Https))
.map_err(fn(_) { HttpClientError("设置请求协议失败") })
.unwrap_or_error!()
let outgoing_body = request
.body()
.map_err(fn(_) { HttpClientError("获取请求体失败") })
.unwrap_or_error!()
let stream = outgoing_body
.write()
.map_err(fn(_) {HttpClientError("打开请求体流失败")
})
.unwrap_or_error!()
let _ = stream
.blocking_write_and_flush(payload!(recipients))
.map_err(fn(error) {HttpClientError("写入请求体失败: \{error}")
})
.unwrap_or_error!()
let _ = outgoing_body
.finish(None)
.map_err(fn(_) { HttpClientError("关闭请求体失败") })
.unwrap_or_error!()
request
变量,包含了发送 HTTP 请求所需的所有内容,接下来我们可以调用 handle
函数来发起 HTTP 请求:
let future_incoming_response = @outgoingHandler.handle(request, None)
.map_err(fn(error) { HttpClientError("发送请求失败: \{error}") })
.unwrap_or_error!()
while true {match future_incoming_response.get() {Some(Ok(Ok(response))) => {let status = response.status()if status >= 200 && status < 300 {break
} else {
raise HttpClientError("HTTP 请求返回状态码 \{status}")
}
}Some(Ok(Err(code))) =>
raise HttpClientError("HTTP 请求失败,错误代码: \{code}")Some(Err(_)) => raise HttpClientError("HTTP 请求失败")None => {let pollable = future_incoming_response.subscribe()let _ = @poll.poll([pollable])
}
}
}
调试
golem app build --build-profile debug
),Golem 会在 MoonBit 组件出现问题时显示详细的堆栈追踪。另一种观察 worker 的有用方法是,在其中写入日志,可以通过 golem worker connect
或 Golem 控制台实时查看(或稍后查询)。
import wasi:logging/logging;
"demo/archive/interface/wasi/logging/logging"
let recipients = self.get_recipients();
@logging.log(@logging.Level::INFO, "", "发送电子邮件到以下收件人: \{recipients}")
match send_emails?(recipients) {Ok(_) => @logging.log(@logging.Level::INFO, "", "发送电子邮件成功")Err(error) => @logging.log(@logging.Level::ERROR, "", "发送电子邮件失败: \{error}")
}
结论
wit-bindgen moonbit
生成的目录结构刚开始时确实让人感到有些压倒性。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
TIOBE 2024 年度编程语言:Python
TIOBE 宣布 2024 年度编程语言花落 Python,该语言在 2024 年的涨幅高达了 9.3%;远远领先于其竞争对手:Java +2.3%、JavaScript +1.4% 和 Go +1.2%。 TIOBE CEOPaul Jansen点评道,“如今 Python 无处不在,它是许多领域无可争议的默认语言。它甚至可能成为 TIOBE 指数中排名最高的语言。Python 唯一的严重缺点(因此为竞争留下了空间)是性能不足,并且大多数错误发生在运行时。” 纵观2024 年的 TIOBE 指数榜单,前 10 名中还发生了两件值得注意的变动:包括 C 语言被 C++ 和 Java 超越,主要原因是 C 在许多嵌入式软件系统中被 C++ 取代;以及 PHP 最终跌出 top 10,被 Go 所取代。 Rust 和 Kotlin 是两种备受关注的语言。Rust 在 2024 年变得越来越流行,但与之相反,Kotlin 在榜单中并没有取得突破,“甚至在 2024 年(可能永远)失去了前 20 名的位置”。不过 Paul Jansen 也指出,尽管 Rust 发展速度惊人,但其陡峭的学习曲线...
-
下一篇
雷军宣布 1000 万元重奖工程师
小米集团创始人、董事长兼CEO雷军宣布,小米公司最高技术荣誉重磅升级为“千万技术大奖”,奖金投入增加至1000万元,以更大的诚意持续重奖工程师。 经过激烈评比,“小米超级电机V8s”技术凭借在创新性、领先性和影响力三个维度的突出表现,荣获2024年小米“千万技术大奖”最高奖项。
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Hadoop3单机部署,实现最简伪集群
- MySQL数据库在高并发下的优化方案
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- 设置Eclipse缩进为4个空格,增强代码规范
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS7,CentOS8安装Elasticsearch6.8.6
- SpringBoot2全家桶,快速入门学习开发网站教程