Skip to content

Java SDK

Java SDK,一个客户端,每个产品(方块、沙箱、浏览器、云盘、数据库)一个命名空间,外加平台本身的调用。支持 Java 17 及以上,基于 JDK 自带的 java.net.http,没有任何依赖。

更新于

安装

需要 Java 17 或更高版本,基于 JDK 自带的 java.net.http(HTTP 和 WebSocket);没有依赖,也没有框架。调用是阻塞式的,错误抛出 GlacException(非受检异常),返回的对象都是 record,选项是链式的 …Options,时长为 Duration,时间为 Instant。它在 Maven Central 上名为 ai.glac:glac,位于 ai.glac 包中:

<dependency>
  <groupId>ai.glac</groupId>
  <artifactId>glac</artifactId>
  <version>0.1.0</version>
</dependency>

客户端

Glac glac = new Glac(); // GLAC_API_KEY and GLAC_API_URL; or Glac.builder().apiKey(key).baseUrl(address).build()

App app = glac.cube().deploy("ghcr.io/acme/api:1.4", new AppOptions().name("api").port(8080)); // Cube: waits until it serves
System.out.println(app.url());

Sandbox sbx = glac.sandboxes().create(); // Sandbox: stays until deleted
Result res = sbx.run("python3 -c 'print(6*7)'"); // res.stdout(), res.stderr(), res.exitCode()
System.out.println(res.stdout() + " " + sbx.url(3000));

Drive drive = glac.drives().create(new DriveOptions().name("data").sizeBytes(10L << 30)); // Drive

System.out.print(Sandbox.runOnce("echo hello").stdout()); // the shorthand: a sandbox for one command, on the default client

glac.cube().deploy 就是 glac deploy:为名为 api 的应用部署一个新版本(没有这个应用时先创建它),并等到它承接应用的流量。最后一行是唯一的简写,即沙箱 SDK 都有的、无需客户端的一行写法。除了一次性的沙箱和你自己调用的 delete(),什么都不会被删除;Sandbox 没有实现 AutoCloseable,因为沙箱本来就是要保留的。

new Glac() 从 GLAC_API_KEY 读取密钥,从 GLAC_API_URL 读取地址(默认 https://api.glac.ai)。要使用其他地址、密钥或超时,请构建一个客户端:

Glac glac = Glac.builder().apiKey(key).baseUrl(address).build();
Sandbox named = glac.sandboxes().create(new CreateOptions().name(name).env("STAGE", "dev").neverSleep());
调用 作用
new Glac(), Glac.builder(): apiKey, baseUrl, timeout, connectTimeout, retries, onResponse 地址、密钥、超时和重试。任意多个线程都可以共享一个客户端。
Glac.shared() 简写所用的客户端:第一次使用时根据环境变量创建,与 new Glac() 相同。
glac.cube() 方块:应用(见下文)。glac.workers() 是它的另一个名字,标记为 @Deprecated。
glac.sandboxes(), .snapshots(), .sshKeys() 沙箱、沙箱的快照,以及能打开沙箱的 SSH 密钥(见下文)。
glac.drives(), glac.volumes(), glac.backups() 云盘(属于控制平面)及其快照、云盘底层的存储卷,以及存储卷的备份(属于控制平面);见下文。
glac.domains() 项目的自定义域名,属于控制平面(见下文)。
glac.webhooks(), glac.keys() 项目的事件发往哪里,以及项目的 API 密钥,属于控制平面(见下文)。
glac.destinations() 项目中应用的日志、指标和追踪导出到哪里,属于控制平面(见下文)。
glac.secrets(), glac.variables() 项目的机密和共享变量(见下文)。
glac.deploys() 项目的部署(/v1/deploys,属于控制平面):多个服务一起部署,按各自等待的对象排序,并一起切换(项目部署)。create(Map.of("api", new DeployOptions().image("ghcr.io/acme/api:2"), "web", new DeployOptions())) 给每个服务单独部署时接受的内容,并等待部署完成 (Deploy.await()),跟随取代它的部署;失败时抛出类型为 deploy_failed 的 GlacException,附带部署的错误码(job_failed、job_timed_out、job_lost 等),服务作为 param() (services.<name>)。还有 create(services, new ProjectDeployOptions().noWait())、get(id)、list()。
glac.account(), updateAccount, usage, usageReport, billing, updateBilling, balance, updateBalance, topUp(usd), charges, ledger, project, events 平台本身的、不属于任何产品的调用(见下文)。
glac.request("GET", "/v1/webhooks", null, null) SDK 没有封装的 API:返回它的响应 JSON。

命名空间是返回对象的方法,这些对象由 Glac 创建,每个客户端一个。对象沿用 API 的名称:方块是 App(API 称之为 app),还有 Sandbox、Drive、DriveSnapshot、Volume、Backup、Domain、Snapshot、SshKey、Secret、Variable、Version、Instance、Deploy。

方块

方块(API 称之为 app)是作为服务运行在自己地址上的镜像,实例数量随请求的需要而定,没有请求时一个都没有。每次部署都会产生一个版本,版本就绪后承接应用的流量。应用属于控制平面:对着一个独立运行的节点,每个调用都会得到该节点自己的 404 no_such_route。

调用 作用
glac.cube().deploy(image), deploy(image, new AppOptions().name("api")…) 与 glac deploy 相同。项目中没有该名称的应用时:用给出的所有选项创建应用及其第一个版本。应用已存在时:先用 PATCH 修改给出的立即生效的设置(区域、扩缩容、键、出站规则、日志、追踪等),只在给出其中某些设置时才修改;然后用给出的内容创建一个版本,没给出的沿用应用已有的 (POST …/versions)。在此期间被别人占用的名称 (409 name_taken) 视为部署到他们的应用;不给名称时,会创建一个由 API 命名的新应用。除非 noWait(),否则会等待版本,然后返回重新读取的应用(url()、currentVersion())。
glac.cube().create(AppOptions), get(idOrName), getOrCreate(name, AppOptions), list() 创建一个应用(给出镜像时,会等待它的第一个版本);查找一个;查找或创建一个(已存在的按原样返回);列出。drives(List.of(new DriveMount(…))) 是应用唯一的实例以读写方式挂载的云盘(有多个实例或使用键的应用会被拒绝,409 drive_single_writer;快照的只读挂载只供沙箱使用,在这里带快照的挂载会被拒绝,400 unknown_field);keyDrives(List.of(App.KeyDrive.of(name, path))) 给键控应用的每个键的实例各自的云盘,随该键的第一个实例创建,随键删除。
glac.cube().logs(idOrName, LogsOptions) 按 id 或名称读取应用的日志,无需先获取应用。
app.deploy(new DeployOptions().image(…)), app.update(AppChanges), app.rollback("1"), app.versions(), app.delete() 为手上的应用部署一个版本;修改;把流量切回某个版本;列出它的版本;删除。
AppOptions 和 DeployOptions 的 .runtime(Map.of("ulimits", Map.of("nofile", 65535))) 版本的运行时设置,与 API 接受的形式相同:ulimits、sysctls、tmpfs、dns、shm_size_bytes;整体替换。
app.logs(new LogsOptions().follow()), app.logUsage(…), app.deleteLogs(before), app.info().logsState() 日志(一个 Iterable<LogLine>,在连接丢失和控制平面重启时都能保持);日志的统计;删除日志行;保存其日志的每台机器,现在是否长期保留那里打印的内容(日志)。
app.instance(key, region), app.getInstance(key), app.instances(), app.deleteInstance(key) 键控实例:每个键一个实例,创建或唤醒。
instance.upgrade("3"), app.upgradeInstance(key, version); upgrade.await(), await(limit); instance.upgrades(), app.instanceUpgrades(key), app.instanceUpgrade(key, id) 把某个键的实例原地迁到应用的另一个版本:null 表示应用的当前版本,否则为版本的 id 或编号(必须是就绪的版本,也可以是更旧的)。实例保留它的 id、节点、键、地址、令牌和它自己的云盘;它的磁盘、机器和进程换成新版本的,切换期间它的请求暂存在边缘节点。返回进行中的 Upgrade(它的 info():status 为 preparing、switching、done 或 failed,fromVersion、toVersion、timings、error、putBack、kind(upgrade 或 revert)、when、keepDays、rollback(回退它所需的内容,保留 keepDays 天)、reverts、batch;通过 API 要求等实例休眠的升级,在那之前为 waiting,被取消时为 cancelled(API 的 DELETE,或它的批次被停止):被取消的升级就此结束,等待它的调用会像升级失败那样抛出异常,错误码为 cancelled);实例已经在那个版本上时返回 null。新版本没能起来时,await() 抛出类型为 upgrade_failed 的 GlacException(错误码为 upgrade_failed 或 upgrade_not_here),实例留在原来的版本上。
app.upgradeInstanceWith(key, new UpgradeOptions().version("4").whenAsleep().keepDays(14)), instance.upgradeWith(…) 同上,带 whenAsleep(升级等到实例休眠时再开始,期间为 waiting,这样它的请求一个都不会被暂存)和 keepDays(0 到 365,默认 7:升级完成后回退所需内容的保留天数,按快照计费)。如果版本的服务挂载的云盘或存储卷与实例的不同,就不能原地升级:升级以 failed 结束,错误码为 upgrade_not_here。
upgrade.cancel(), app.cancelInstanceUpgrade(key, id); upgrade.revert(), upgrade.revert(keepDays), app.revertInstanceUpgrade(key, id, keepDays) 取消一个正在等待的升级(等实例休眠,或在批次中排队):状态变为 cancelled,实例留在原来的版本上(已经开始的升级返回 409 upgrade_started)。回退:把实例恢复到升级所保留的状态,即它的磁盘、升级之前的云盘(会先为云盘现在的内容创建快照)和原来的版本;返回一个新的 Upgrade,类型为 revert,它的进行和等待方式与升级相同。要回到的内容已不再保留时,返回 409 rollback_gone。
app.upgradeInstances(new UpgradeBatchOptions().version("4").fromVersions(List.of("3")).keys(List.of("k")).whenNow().keepDays(1).concurrency(8).maxFailures(1)); app.upgradeBatches(), app.upgradeBatch(id), app.stopUpgradeBatch(id); batch.await(), await(limit), batch.refresh(), batch.stop() 把应用的多个键批量升级到同一个版本(默认为应用的当前版本):每个键在其实例休眠时升级(或轮到时立即升级,whenNow),默认同时 4 个(同一节点上 2 个),默认失败 5 次后以 failed 停止。返回一个 UpgradeBatch,它的 info() 是一个 UpgradeBatch.Info:status(running、done、failed、stopped)、stopReason、counts、upgrades(每个成员的键、升级、状态和错误)、skipped、origin。无论批次如何结束,await() 都返回它;实例从不休眠的成员会让批次一直运行,所以 await(limit) 会抛出 wait_timeout。stop() 取消仍在等待的成员。
app.openInstancePort(key, "tcp", port, "2001:db8::/32"), app.instancePorts(key, "tcp"), app.closeInstancePort(key, "tcp", port) 某个键的实例在它自己的公网 IPv6 地址上开放的端口,无论实例在哪里:按应用和键调用它的沙箱的 openPort、ports 和 closePort("udp" 表示 UDP;键控实例)。没有自己地址的实例会被拒绝 (409 no_ipv6)。
AppOptions 和 AppChanges 的 checkpoint(n),AppChanges.noCheckpoint() 应用的键控实例在运行期间的检查点(每 n 秒一次,1 到 600;0 表示用默认值,每 10 秒一次),以及关闭检查点;App.Info.checkpoint() 会说明它。实例的 SandboxInfo.checkpoint()(一个 SandboxInfo.Checkpoint:everySeconds、lastAt、behindSeconds、bytesPerSecond、storedBytes、error;没有检查点时为 null)说明它的检查点进行得如何。
app.metrics(from, to, step) 为应用提供服务的机器对它的统计,每分钟一步("hour":每小时一步;null:最近一小时,按分钟):按状态码类别统计的请求数、耗时、实例及其用量(指标)。返回 API 的对象。
AppOptions 和 AppChanges 的 traceSampleRate(0.1)、trustIncomingTrace(true);app.info().traces() 平台为应用的一部分请求创建 span,实例自己的 span 是它的子 span(默认不创建),发往项目中接收 "traces" 的导出目标(追踪);应用当前的设置(一个 App.TraceSettings)。
app.createKeySecret(), app.keySecrets(), app.keySecret(id), app.deleteKeySecret(id), KeySecret.signKey(secret, appId, key, expiresAt) 用来签名应用的键的机密(一个 KeySecret,只有刚创建的那个才有 secret()),以及用它签名的键,即经由边缘节点的请求所携带的形式(签名键);KeySecret.pathSegment 把它写成路径段。

没能起来的版本会抛出 GlacException,type() 为 deploy_failed,并附带版本自己的错误码和输出;流量保持不变。构成版本的内容(image、startCommand、port、env、secret、vcpus、memoryBytes、volumes、drives、keyDrives 等)就是 DeployOptions 的全部;应用的其他设置立即生效,不产生新版本。

沙箱

调用 作用
glac.sandboxes().create(new CreateOptions().image(…).name(…).env(k, v).drive(new DriveMount("/data", id)).sleepAfter(d).neverSleep().sleepNotice(d).sleepHoldMax(d).egress(…)) 创建一个沙箱;每个选项都可以省略。启动时运行的服务:start(true)(镜像自己的启动命令)、startLine(…) 或 startArgv(…),配合 readyTcp(port)、readyHttp(port, path) 或 readyCommand(…);hostname(…)、stopSignal(…) 和 stopGrace(d);projectNetwork() 让它从一开始就加入项目的私有网络 (info().projectAddress());new VolumeMount("/data", vol).withCopy() 把镜像在该路径下的内容放进新的存储卷(创建沙箱)。
sbx.info().appId(), versionId(), key(), imageDigest() 通过控制平面创建的沙箱是某个应用的实例:不带镜像创建时,是项目默认的沙箱应用 (sbx-sandbox),否则是它的镜像对应的应用(应用的 instances() 列出该应用的沙箱,每个都在它的键下);它所属的该应用的版本、创建它所用的镜像(之后被移动过的标签会成为应用的新版本,此后创建的沙箱属于新版本);它在那里的键、名称或 id;以及按摘要标识的镜像。创建的响应和之后的每次读取都会给出这些信息,并且保持不变;从模板或快照创建的沙箱,以及分叉,这些信息为 null。
glac.sandboxes().get, getOrCreate, list(ListOptions), listPage(ListOptions) 查找;查找或创建;列出(全部,或一页)。
CreateOptions 和 UpdateOptions 的 suspendAfter(d)、wakeDailyAt("03:30")(defaultSuspendAfter()、noDailyWake() 恢复默认);SandboxInfo.suspendAfter()、wakeDailyAt() 因空闲而停止后,它保持暂停多久才保存内存、真正休眠(一秒到一天;它的节点最多保持十分钟),以及休眠期间每天在 UTC 的哪个时刻唤醒它,以便做它自己等着唤醒才做的工作。
CreateOptions 和 UpdateOptions 的 onExpiry("sleep")、heldAsleep(true);SandboxInfo.onExpiry()、heldAsleep() expiresAt 到期时做什么:delete 删除沙箱(默认)、sleep(让它休眠并保留,请求照常会唤醒它)或 hold(同上,但保持休眠);以及它休眠时,发往它某个端口的请求是否会唤醒它:保持休眠时,它的预览地址返回 502 sandbox_paused,API 调用会唤醒它。
glac.sandboxes().run(command), run(command, RunOptions, CreateOptions) 为一条命令创建的沙箱:创建、运行、删除。
CreateOptions.init("systemd"); SandboxInfo.init() 完整的系统:镜像自己的 systemd 作为沙箱的 PID 1(不指定镜像时,使用平台带 systemd 和 OpenSSH 的 Ubuntu),所以软件包、用 systemctl 启动的服务、cron 和 journal 都能像在一台普通机器上一样工作,它也有自己的 machine ID,在休眠、唤醒和迁移后保持不变。不能与启动命令或就绪检查同时使用 (400 invalid_start),也不能从快照创建。这样的沙箱 init() 为 "systemd",使用平台 init 的沙箱则为 null。
sbx.run(command, new RunOptions().cwd(…).timeout(d).onStdout(…)) 运行并等待。Result 包含 stdout、stderr、exitCode、duration、usage、wake。
sbx.exec, sbx.terminal() 启动一条命令(waitFor()、output()、write()、kill());一个终端。
sbx.files() read、readText、open、write、list、stat、exists、mkdir、move、remove,以及 setAttributes(path, mode, sizeBytes, modifiedAt):文件的权限、长度和最后修改时间(null 表示保持不变);.asUser("node")。
sbx.url(port), sbx.token(scopes, expiresIn) 端口链接(在本地签名);这个沙箱的令牌。
sbx.info().ipv6(), sbx.openPort("tcp", port, from…), sbx.ports("tcp"), sbx.closePort("tcp", port), inbound(InboundPort.tcp(port, from…)) 它自己的公网 IPv6 地址(如果有,否则为 null);向列出的来源(IPv6 前缀或地址,"::/0" 表示所有人)开放一个端口,并给出访问地址;开放的端口;关闭一个端口(并说明它之前是否开放);CreateOptions 和 UpdateOptions 中的列表。入站默认关闭;它休眠时和迁移时,开放的端口都保持开放。参见网络。
sbx.revokeToken(token) 从此只拒绝那一个令牌,其他令牌照常有效:一个 Token(它的 id(),即 tok_…,指明是哪个)、令牌本身,或它的 id。一个沙箱最多保留 1,000 个尚未过期的这类吊销(超出时返回 409 too_many_revoked_tokens:这时请用 revokeTokens())。返回一个 TokenRevocation,带有 tokenId() 和 expiresAt()。
sbx.revokeTokens() 到目前为止为这个沙箱发出的所有令牌都会失效 (401 invalid_token),包括预览链接和 Cookie 中的令牌,以及 SSH 会话持有的令牌;之后创建的令牌有效。需要 API 密钥。返回一个 TokenRevocation:sandboxId()、generation()、revokedAt()。
sbx.update, sbx.sleep(), sbx.restart(), sbx.delete(), sbx.snapshot(), sbx.fork(n), sbx.restore(…) 修改;休眠;从磁盘启动;删除;创建快照;分叉;恢复。
new CreateOptions().checkpoint(10), new UpdateOptions().checkpoint(10), .noCheckpoint(); info().checkpoint() 运行期间的检查点(间隔 1 到 600 秒),这样机器丢失时它能带着进程继续运行;关闭检查点;检查点的进行情况。没有机器支持检查点时返回 409 checkpoints_unavailable。
info().diskWrites() 它的磁盘今天(UTC 日)已写入的量,与当天全速份额的对比(一个 SandboxInfo.DiskWrites):todayBytes(),其中 checkpointCopyBytes() 是检查点副本写入宿主机磁盘的量;fullSpeedUntilBytes(),即份额(默认 100 GiB,节点另有设置时除外;null 表示节点不限速,但照样统计);limited() 和 limitedUntil():超出份额后,它的磁盘以每秒 2 MiB 的速度接受写入,直到当天结束,任何写入都不会失败。在不把磁盘复制到对象存储的节点上为 null,那里什么都不统计。写入它的云盘的数据不计在这里:写入云盘不收费。
glac.sandboxes().snapshots(): get, list; snap.rename(name), snap.expireAt(at) 快照:某一时刻的内存、进程和磁盘;给快照换个名称(null:不要名称),或设置它自行删除的时间(null:永不删除)。
glac.sandboxes().sshKeys(): create(publicKey, SshKeyOptions), get, list 通过 SSH 网关打开沙箱的公钥;私钥会在发送任何东西之前就被拒绝。

简写。Sandbox.runOnce、Sandbox.create、Sandbox.get、Sandbox.getOrCreate、Sandbox.list 和 Sandbox.listPage 就是 Glac.shared().sandboxes() 的调用,是同一套实现。其他产品都没有无需客户端的静态方法。一次性运行在命名空间上叫 run,在类上叫 runOnce,因为 Java 不允许静态方法和实例方法有相同的签名(sbx.run 在沙箱中运行一条命令);等待命令用 waitFor(wait() 属于 Object)。以 String 给出的命令用 sh -c 运行;以 List<String> 给出时,它是一个程序及其参数。

Command cmd = sbx.exec("node server.js"); // do not wait
try (Output out = cmd.output()) {
    for (Piece piece : out) {
        System.out.print(piece.text());
    }
}

云盘

云盘是项目的一块磁盘,不随它的沙箱消失:同一时间由一个沙箱以读写方式挂载,可以在沙箱创建时挂载,也可以在沙箱运行时挂载;沙箱没了,云盘连同其中的内容一起保留,供下一个沙箱挂载:可以在它的区域,也可以在别的区域,只要那里的机器能读取它归属区域的对象存储(见下文)。云盘在写入的同时被复制到所在区域的对象存储中,所以其他节点上的沙箱也能挂载它:会先把它搬过去。只按写入的数据计费 (usedBytes)。它的快照是它在那个存储中的版本,创建快照不复制任何数据:分叉是从快照创建的新云盘,恢复是把云盘带回某个快照,任意多个沙箱都可以同时以只读方式挂载一个快照(见下文)。云盘属于控制平面 (/v1/drives):对着一个独立运行的节点,每个调用都会抛出该节点的 404 no_such_route,节点上的磁盘是存储卷(见下文)。

Drive data = glac.drives().create(new DriveOptions().name(name).sizeBytes(1L << 30));
Sandbox with = glac.sandboxes().create(new CreateOptions().drive(new DriveMount("/data", data.id())));
with.run("echo kept > /data/note");
glac.drives().detach(with.id(), data.id());
Sandbox other = glac.sandboxes().create();
glac.drives().attach(other.id(), new DriveMount("/data", data.id()));
System.out.println(other.run("cat /data/note").stdout()); // kept
调用 作用
glac.drives().create(new DriveOptions().name(…).sizeBytes(…).region(…).durability(…)), get(idOrName), list(), listPage(ListOptions) 创建一个云盘,里面只有文件系统;每个选项都可以省略(sizeBytes:4 KiB 块的整数倍,最小 16 MiB;默认 64 GiB,免费版上为 5 GiB;region:na、eu 或 ap;durability:standard 或 strict,后者要等对象存储收到 fsync 之前的每一次写入,客户机的 fsync 才返回)。查找一个;列出,最新的在前;获取一页。
glac.drives().rename(drive, name), glac.drives().delete(drive) 重命名(null:不要名称);删除云盘及其中的内容。
glac.drives().attach(sandbox, new DriveMount(path, drive)), detach(sandbox, drive), forceDetach(sandbox, drive) 立即以读写方式把它挂载到一个已有的沙箱中(休眠的沙箱会被唤醒);卸载它(有进程在使用时用 forceDetach)。每个调用都返回云盘。
drive.attach(sandbox, path), drive.detach(sandbox), drive.rename(name), drive.refresh(), drive.delete() 同样的调用,也可以在 Drive 上进行,它的 info() 是一个 Drive.Info:id(vol-…:云盘就是存储卷)、name、sizeBytes、usedBytes、attachedTo(Drive.Attachment:沙箱、路径、模式)、region、home、status、lease(Drive.Lease:唯一写入方所在的节点、纪元和到期时间)、appId 和 key(键控实例的云盘)、parentDriveId 和 rootDriveId(分叉的云盘才有:来源云盘和这一脉的第一个云盘,来源云盘之后可能已被删除;不是分叉的云盘为 null)、managedBy(一个 ManagedBy,即这个云盘所属数据库的 id;不属于任何东西时为 null)、createdAt。数据库的云盘只能由数据库的实例以读写方式挂载,重命名、恢复和删除都要通过数据库进行:在这里调用这些操作会被拒绝 (409 managed_by_database);项目可以为它创建快照和分叉。构成数据库的应用也会以同样的方式说明这一点 (app.info().managedBy()),它的更新、部署、回滚和删除同样会被拒绝。
glac.drives().snapshot(drive), snapshot(drive, new DriveSnapshotOptions().name(…).fsConsistent()), drive.snapshot() 为云盘创建快照,即一个 DriveSnapshot:云盘在对象存储中的一个版本,会被保留;不复制任何数据。运行中的沙箱以读写方式持有的云盘,会在客户机的写入被暂停片刻时创建快照,最多十秒(超过时返回 503 unavailable,可重试):fsConsistent() 让沙箱的代理程序冻结它的文件系统(consistency 为 frozen;代理程序做不到时返回 409 not_frozen:这时请不带它创建),不带它时则暂停机器(paused,相当于断电);没有运行中的沙箱持有的云盘为 at_rest。节点不向对象存储复制任何数据的云盘没有快照 (409 not_copied)。
glac.drives().snapshots(drive), allSnapshots(driveDeleted), getSnapshot(drive, snapshot), deleteSnapshot(drive, snapshot), drive.snapshots() 一个云盘的快照,最新的在前;项目中所有云盘的快照(true:只列出已删除云盘的快照;null:全部);按 id 查看一个 (404 snapshot_not_found);删除一个。
glac.drives().fork(drive, new ForkOptions().name(…).fromSnapshot(snapshot)), drive.fork(…), snap.fork(name) 一个新云盘,它的第一个版本是快照的内容,或云盘当前的内容;不复制任何数据,可以立即挂载。它的 parentDriveId 和 rootDriveId 说明它从何而来;它和创建一样受套餐云盘数量的限制 (403 drive_limit_reached)。
glac.drives().restore(drive, snapshot), drive.restore(snapshot), snap.restore() 在云盘最新的版本之后写入一个新版本,内容与快照相同;中间的版本只要有快照或分叉保留着就仍然存在,所以先恢复到较旧的快照、再恢复到较新的快照也可以。沙箱以读写方式持有的云盘会被拒绝 (409 drive_attached):先卸载它。
new DriveOptions().historySeconds(n), glac.drives().setHistory(drive, n), versions(drive), versionAt(drive, t), new ForkOptions().at(t), restoreTo(drive, t); drive.setHistory, drive.versions(), drive.restoreTo(t) 云盘的版本窗口 (slice DB8):按时间最多能往回恢复或分叉多远,3600 到 86400 秒(不设置时没有窗口;关闭后,它保留的内容会被丢弃)。在每个有写入的 30 秒里,窗口都会得到一个版本,即云盘在某一时刻的状态;这些版本单独保留的数据和快照一样计费。按时间恢复或分叉时,取该时间点或之前最新的版本,也就是以该时间调用版本列表时给出的那个版本。versions 返回 Drive.WindowVersion,最新的在前;setHistory(drive, null) 关闭版本窗口;Drive.Info 中的 historySeconds、history。
new DriveOptions().cacheBytes(n), glac.drives().setCache(drive, n); drive.setCache(n), drive.info().cacheBytes(), drive.info().cachedBytes() 云盘的缓存:节点在 NVMe 上最多保存云盘的多少数据。默认全部保存(即云盘的大小);也可以设得更小,最小到 0,超出的部分只保存在对象存储中,客户机读到时再从那里读取(最久没被读过的数据最先释放,对象存储还没有的数据从不释放)。缓存按小时、按已填充的量计费,与云盘的数据费用分开。调小缓存后,超出的部分一旦已在对象存储中就会被释放(如果运行中的沙箱挂载云盘时是全部缓存,则从那个沙箱下一次休眠或停止时开始);调大缓存后,从那时起客户机读到的数据都会留在缓存中。API 把云盘叫作存储卷 (/v1/volumes);各客户端通过 /v1/drives 访问它们,这个接口的响应和以前一样。setCache(drive, null) 恢复为全部缓存。控制平面没有给出缓存信息时,cacheBytes() 就是云盘的大小;cachedBytes() 是当前在节点 NVMe 上的量(没有节点持有云盘时为 null)。用量报告的分组和合计中有 cacheGbHours()。
glac.drives().attachSnapshot(sandbox, new DriveMount(path, drive).withSnapshot(snapshot)), detachSnapshot(sandbox, snapshot), forceDetachSnapshot(sandbox, snapshot), snap.attach(sandbox, path), snap.detach(sandbox) 以只读方式把快照挂载到一个已有的沙箱中;按快照的 id 卸载它。每个调用都返回快照。创建沙箱时:new CreateOptions().drive(new DriveMount(path, drive).withSnapshot(snapshot)),或 new DriveMount(path, snap)。
snap.info(), snap.refresh(), snap.delete() 一个 DriveSnapshot.Info:id (dsnap-…)、driveId、name、consistency、usedBytes(当时云盘中的数据量)、exclusiveBytes(它单独保留的数据量)、exclusiveAt、version、createdAt。
  • 一个云盘同一时间只属于一个沙箱:另一个沙箱持有的云盘会被拒绝 (409 drive_attached),删除已挂载的云盘也一样。键控实例的云盘随它的键一起删除 (409 key_drive)。
  • 另一个节点持有的云盘,会先被搬到沙箱所在的节点;对象存储中还没有它的最新状态时 (503 copy_behind),或无法把它搬过去时 (503 drive_not_brought),调用可重试:客户端会按 retries 的设置重试,之后抛出异常,你稍后再请求一次即可。
  • 跨区域 (slice D9)。云盘的归属区域 (home) 是保存其副本的对象存储所在的区域:也就是创建它的区域。另一个区域的沙箱也能挂载它,前提是那个区域的机器能读取其归属区域的存储(不能时返回 409 not_on_one_machine):云盘被释放为一个版本,在那里被接管,客户机读取时从归属区域的存储读出,客户机写入的数据则进入它现在所在区域的存储 (region)。云盘在那里待满一小时后,归属区域会跟过去,用 glac.drives().moveHome(drive) (drive.moveHome()) 则立即迁移:它的机器把归属区域存储中剩下的内容复制到自己区域的存储中,那里什么都不剩时,home 就变成新区域;Drive.Info#homeMove 说明迁移进行到哪一步(asked_at、asked_by、copied_objects、copied_bytes、objects_left、error)。保留的版本都不再需要旧归属区域的副本后(一天后),这份副本会被删除。云盘的归属区域最多每六小时迁移一次 (409 home_moved_recently);归属区域就是当前所在区域的云盘返回 409 home_here。
  • 套餐的限制:免费版最多 3 个云盘,单个最大 10 GiB,合计 15 GiB;付费版最多 500 个,单个最大 1 TiB (403 drive_limit_reached)。
  • new UsageOptions().drive(id)、kind("drive") 和 groupBy("drive") 报告云盘保存了多少、写入了多少、冷读了多少。drive.created、drive.deleted、drive.attached 和 drive.detached 事件携带 Event.Drive 或 Event.DriveAttached。
  • 快照不随云盘消失:云盘删除后,仍可以按云盘的 id 列出、读取、分叉它的快照,或以只读方式挂载。快照按它单独保留的数据 (exclusiveBytes) 计费,按小时计,价格与云盘的数据相同(每 GB·月 $0.02)。
  • 分叉和任何云盘一样,按它的 usedBytes,即完整的逻辑内容计费:从 10 GB 快照分叉出的云盘按 10 GB 的云盘计费。
  • 只读挂载。在云盘所在区域的任意机器上,任意多个沙箱都可以同时挂载同一个快照,无论云盘被谁持有,云盘删除后也可以。数据块在客户机读取时从对象存储取来:这些冷读不收费,并在用量中计在那个沙箱名下(glac.usageReport(new UsageOptions().sandbox(id)) 的 driveColdReadBytes());读取那个沙箱的机器上已有的数据也不收费。客户机不能写入它,挂载了快照的沙箱也不会迁到别的机器上。attach 不接受带快照的挂载,attachSnapshot 不接受不带快照的挂载,detachSnapshot 只接受 dsnap-… 形式的 id:这些都会在发送任何东西之前就被拒绝。

glac.volumes() 对应 API 的 /v1/volumes,即云盘底层的磁盘,为过渡期而保留:只有它才有的,是独立运行的节点(节点上的磁盘就是存储卷),以及共享存储卷,即可由多个沙箱同时挂载的文件系统。控制平面不会为项目的密钥创建普通存储卷。它有 create()、create(VolumeOptions)(name、sizeBytes、shared)、get(idOrName) 和 list();vol.delete() 删除一个没有沙箱持有的存储卷(有沙箱持有时返回 409 volume_in_use)。沙箱用 new CreateOptions().volume(new VolumeMount("/team", vol.id())) 挂载存储卷,应用的实例用 AppOptions.volumes(…)。

存储卷的备份。备份是存储卷(云盘也是存储卷)在某一时刻的副本,保存在对象存储中,直到过期:可以由调用创建,可以按存储卷的规则每天创建,也会在服务的新版本替换其实例之前创建。与云盘的快照不同,它是一份独立的副本,仅凭它就能创建存储卷,而且不随存储卷消失;存储卷的备份只保存自上一个备份以来变化的内容,多个备份都需要的数据块只计费一次,计在最早的备份上。在存储收到全部数据之前为 making,之后为 ready,从那时起按存储中保存的数据量 (storedBytes)、以休眠沙箱所保存内容的价格计费(备份)。备份属于控制平面。

调用 作用
glac.volumes().backup(idOrName), backup(idOrName, new BackupOptions().expiresInDays(7)), vol.backup(), glac.volumes().backups(idOrName) 立即创建一个备份,即一个 Backup,保留这么多天(1 到 365),或按存储卷的规则(未设置时为 30 天);列出一个存储卷的备份,最新的在前(已删除的存储卷按 id 指定)。不向对象存储复制任何数据的节点没有备份 (409 not_copied)。
glac.backups().list(), listOf(volumeId), get(id), delete(id), restore(id, name) 项目的备份,最新的在前,包括已删除存储卷的备份;一个存储卷的备份;查看一个;删除一个(之后不再为它计费;从它创建的存储卷仍然保留);从一个就绪的备份创建新存储卷(name 可以为 null),等同于 glac.volumes().create(new VolumeOptions().fromBackup(id))(创建期间返回 409 backup_not_ready,失败后返回 409 backup_unusable)。
b.refresh(), b.restore(name), b.delete() 同样的调用,也可以在 Backup 上进行,它的 info() 是一个 Backup.Info:id (bak-…)、volumeId、volumeName(当时存储卷的名称)、status(making、ready、failed)、reason(manual、daily、before_version)、consistency(frozen、crash)、frozenMs(存储卷的写入方为复制等待了多久;没有冻结时为 null)、sizeBytes(存储卷当时的数据量)、volumeSizeBytes(它最多能容纳的量,也就是从备份创建的存储卷的容量)、storedBytes(计费依据)、ownBytes(删除它能省下的量)、sharedBytes、shared、region、failure、createdAt、readyAt(从此开始计费)、expiresAt。
  • 从备份创建的存储卷,大小和类型与备份所来自的存储卷相同,位于备份所在的区域。它的 info().status() 在完整拿到备份之前为 restoring,之后为 ready,在此之前不能挂载;fromBackup() 指明来源备份。如果被分配到的机器无法恢复它,控制平面会在该区域的另一台机器上再创建一次:这时 restoreAttempt() 为 2,firstFailure() 说明第一次为什么失败。如果这一次也失败,它就是 failed (failure()),只能删除。
  • new UsageOptions().backup(id)、kind("backup") 和 groupBy("backup") 报告备份保存了多少,计在状态计量项上 (state_gb_hours);glac.billing() 的 meters().state().backupsGbHours() 说明该计量项中有多少来自备份。

域名

项目的域名是一个指向应用(和它的默认地址一样)或沙箱某个端口的名称。域名属于控制平面。

调用 作用
glac.domains().create(domain, new DomainOptions().app(…)) 或 .sandbox(…).port(…),配合 .proxied(…);get、list()、list(new ListOptions().app(…))、delete;domain.update(…)、domain.refresh()、domain.delete() 为应用或沙箱端口注册的名称,在 DNS 上能查到它的 verification 所要求的记录之前为 pending_verification(自定义域名);项目中已有该域名时返回 409 domain_exists。

浏览器

尚未开放。浏览器还没有向账户开放:下面是开放之前 SDK 目前的样子。

会话是一个托管的 Chromium,由 Chrome DevTools Protocol (CDP) 客户端从外部驱动:Playwright for Java 用 playwright.chromium().connectOverCDP(session.cdpUrl()) 连接。有客户端连接时它保持运行,最后一个客户端离开几秒后进入休眠,页面会保留。地址里带着凭据,所以 toString() 不包含地址。

  • glac.browsers().create(new BrowserOptions().name("agent-42"))(或 create())创建一个处于运行状态的会话;BrowserOptions 有 kind(默认为 chromium-headless;chromium 有屏幕;lightpanda 是没有布局引擎的浏览器)、name、region、sleepAfterSeconds、connectionIdleSeconds、deleteAfterIdleSeconds、metadata、profile(name, save) 和 profile(name, version, save)(在返回会话之前加载项目的一个配置文件;设置了 save 时,会话删除时再保存回去)、proxy(url)(你自己的 HTTP 代理,需要接受 CONNECT,付费版可用:会话到 80 和 443 端口的连接经由浏览器旁边的一个中继从它出站,凭据由平台保管;免费版上返回 403 proxy_not_on_plan)。如果用已有会话的名称创建,会返回那个会话:它会被唤醒,页面都还在。沙箱在运行前就失败的会话不算会话:创建会被拒绝,返回 503 session_failed。
  • glac.browsers().get(idOrName)、delete(idOrName)(用需要保存的配置文件创建的会话会先保存它)和 deleteWithoutSavingProfile(idOrName),以及会话自己的 delete() 和 deleteWithoutSavingProfile()。BrowserSession 包含 id()、status()、cdpUrl()、profile()(一个 BrowserSession.Profile:name、save、version)、proxy()(一个 BrowserSession.Proxy:它自己的代理的 url 和 username,从不包含密码)、proxyBytes()(双向经过代理的流量)和 info():类型、liveUrl、时间设置、awakeSeconds、proxySentBytes 和 proxyReceivedBytes。
  • status() 为 awake、asleep、unavailable(它的节点无法访问)或 failed(创建得到响应之后它的沙箱失败了:比如浏览器停止且没有恢复),这时 error() 说明原因(一个 BrowserSession.Failure:code、message,按节点的原话;没有失败时为 null)。browser.failed 事件也会说明这一点:Event.BrowserFailed.of(event) 读取它的数据。

会话和任何实例一样,按它使用的方块资源计费:计算时的 CPU、运行时的内存和出站流量(价格与套餐)。session.info().awakeSeconds() 是它已运行的时长。

列出、修改、让会话休眠和换新地址,只有 Python 和 TypeScript SDK 以及 glac browsers 才有。会话属于控制平面。

数据库

数据库是托管的 Postgres:运行在云盘上的独立 PostgreSQL,没有任何请求时休眠,有连接时被唤醒,会话都会保留。所有者的密码只出现在 create 和 resetPassword 的响应中,任何地方都不保存:只有它们返回的 Database 的 connectionString() 才带有密码。

  • glac.databases().create("shop"),或 create("shop", new DatabaseOptions().version("18")),创建一个数据库并等待它第一次启动(status() 为 ready,或为 creating:这时反复 get 它,直到就绪);DatabaseOptions 有 version、region、databaseName、size(vcpus, memoryBytes)、storage(sizeBytes, durability)、sleepAfterSeconds、suspendAfterSeconds(保存内存、真正休眠之前暂停的秒数,暂停期间连接在一两毫秒内就能得到响应:1 到 86400,默认 300)、wakeDailyAt("03:30")(休眠期间每天在那时唤醒)、metadata。用已有数据库的名称创建时,返回那个数据库,不带密码。glac.databases().setSuspendAfter(idOrName, n) 和 setWakeDailyAt(idOrName, "04:00") 修改这两项,所有分支一起修改;null 把其中任意一项恢复默认(分别为 300 和不唤醒)。免费版数据库的休眠设置是固定的,与 Neon 免费版缩容到零的做法相同:最后一个查询一秒后暂停(sleep_after_seconds 为 1),五分钟后休眠(suspend_after_seconds 为 300);无论创建还是修改,其他值都会以 400 invalid_field 被拒绝。
  • glac.databases().createBranch(idOrName, "preview", parent, expiresAt) 创建一个独立分支(parent 为 null:即 main;expiresAt 为 null:一直保留):它的父分支此刻的状态,不复制任何数据,有自己的 PostgreSQL、端点和云盘,在项目网络上的地址是 <database>-<name>;它的角色和密码与父分支相同 (branch.connectionString(ownerPassword))。deleteBranch(idOrName, branch) 停止它的 PostgreSQL 并删除它的云盘;不能删除 main (409 root_branch),也不能删除有其他分支从它创建的分支 (409 has_branches)。
  • new DatabaseOptions().historySeconds(n)、glac.databases().setHistory(idOrName, n)、restore(idOrName, t)、restore(idOrName, t, exact),以及 createBranch(idOrName, name, parent, expiresAt, at)、createBranch(…, at, exact)(Database 的 historySeconds、history):数据库的历史(slice DB8 和 DB5):根分支按时间最多能恢复到多久以前,或从多久以前创建分支:默认一天;付费版可设为 0(不保留)到 30 天,免费版为一天。恢复会把根分支原地带回过去:恢复到整分钟或某个版本自身的时刻时,使用云盘版本窗口中该时刻或之前最新的版本(每个有写入的 30 秒一个版本,最多保留一天);更早的时刻、精确到秒的时刻,或带 exact 时的那个精确时刻(精确到毫秒),则通过 WAL 归档恢复(把 WAL 重放到那一刻:需要几秒到几分钟,调用会一直等待)。端点、连接字符串和所有者的密码都保持不变,现有连接会断开,恢复之前的状态会作为一个独立分支 main_old_<the restore's moment> 保留;如果新版本在五分钟内不能提供服务,恢复会被撤销 (503 database_restore_failed)。按时间创建分支时,则以那个时刻创建分支 (from: {at, exact})。历史中没有的时间返回 400 invalid_history,错误信息会说明版本窗口和归档分别能追溯到什么时候。精确到秒的时间(或带 exact)只能从归档恢复,否则拒绝,从不退而使用窗口中之前的版本:归档追溯不到时返回 400 invalid_history(数据库不保留归档,或时间早于最早的基础备份),归档的存储拒绝提供分段时返回 503 database_archive_unavailable(可重试);每次拒绝都会指明窗口中该时间或之前的版本,你可以改为请求那个版本自身的时刻。存储给不出所需数据块时,返回 503 drive_unreadable(可重试),不会创建任何东西。如果对象存储中断太久,等待归档的 WAL 超过了云盘为日志预留的空间,这部分 WAL 会被放弃,以保证数据库继续运行:这就是归档中的一个缺口,记录在 history().gaps() 中(Database.Gap:from、to、fromLsn、toLsn):被放弃的 WAL 从何时开始,归档到何时为止无法恢复任何时间(缺口之后第一个基础备份完成的时间;在有基础备份之前为 null),以及缺口在日志中的起止位置。缺口内的时间只能从云盘的版本窗口恢复(整分钟,或某个版本自身的时刻);精确到秒或精确时刻的恢复返回 400 invalid_history。缺口也会以事件的形式报告:首次发现缺口时发出 database.history_gap(数据库运行期间,每个缺口一次),缺口之后归档重新可用于恢复时发出 database.history_resumed;Event.DatabaseGap.of(event) 读取它们的数据(其他类型的事件返回 null):数据库,缺口的 from()、to()(前者为 null)、fromLsn() 和 toLsn(),segments()(放弃了多少个 16 MB 的分段),以及前者才有的 error()(对象存储最后一次拒绝时返回的内容)。
  • new DatabaseOptions().tiering(true) 和 glac.databases().setTiering(idOrName, on) 用来开启或关闭分层存储(slice DB18:付费版可用,且所在区域要有对象网关;有分支的分区已经分层时,关闭会被拒绝,返回 409 tiered_data_exists;每个分支的 PostgreSQL 会为此重启一次);Database 的 tiering 说明是否已开启,以及目前按多少计费。
  • get(idOrName)、list()、listNamed(name)、delete(idOrName, finalSnapshot)(除非 finalSnapshot 为 false,否则它的云盘会保留一个快照七天)、branches(idOrName)(先是 main,然后是独立分支)、branch(idOrName, "main")、sleep 和 wake(某个分支,立即执行)、resetPassword(idOrName, role)(新密码只出现在这次响应中;null 表示数据库的所有者)。
  • glac.databases().roles(idOrName) 列出根分支中可登录的角色,直接从 PostgreSQL 读取(DatabaseRole:name、owner):它的所有者和你创建的角色 (CREATE ROLE … LOGIN),平台自己的角色不在其中。resetRolePassword(idOrName, role) 给其中任何一个角色设置新密码,只出现在这次响应中:一个 DatabaseRoleSecret,带有该角色的 password 和根分支上该角色的连接字符串 (connectionString());所有者的连接字符串由 API 响应的数据库生成。独立分支保留它创建时的密码。
  • db.connectionString() 和 branch.connectionString(password):libpq 和 psql 接受的 URI,API 给出公网地址时用公网的,否则用项目网络上的,并带有所有者的密码(百分号编码)。Database 和 DatabaseBranch 是由 API 字段构成的 record:defaultBranch()、connectionUri()、pooledUri()、privateUri()、endpoint()、state()。

数据库在项目网络上按名称访问,来自它的沙箱(用 project_network 创建)和服务:这就是它的 private_uri。有边缘节点提供 PostgreSQL 服务时,也可以从任何地方通过它的公网地址访问(connection_uri,端口 5432,只支持 TLS;连接池的地址:pooled_uri);没有边缘节点提供、或数据库没有可用来命名的区域时,它们为 null;给出时,连接字符串会使用它们。toString() 既不显示密码,也不显示连接字符串。数据库属于控制平面。

托管应用

目录中的应用(比如 Rivet)以它自己的方块资源安装到项目中:它的应用、存储卷、机密和存储桶,都带有该安装的标签,只能通过这个安装修改,并按这些资源本身计费(托管应用)。安装的响应包含全部输出,包括机密输出(管理员令牌、带有令牌的端点),之后的读取只对有 admin 权限范围的密钥显示这些输出:安装对象会保留之前的响应给出、而之后的响应隐藏的每个输出。

Catalogue.App rivet = glac.catalogue().get("rivet");      // its parameters, and a month of it in Cube's meters
Installation engine = glac.installations().create("rivet", new InstallationOptions().parameters(Map.of("disk_gib", 20)));
System.out.println(rivet.month() + " " + engine.info().outputs().get("endpoint"));   // RIVET_ENDPOINT, its token in it
调用 作用
glac.catalogue().list(), glac.catalogue().get(app), get(app, version) 目录中的应用,每个都是一个 Catalogue.App(app、version、title、summary、verified、licence、versions、parameters:一个 JSON Schema,outputs、month,完整的条目在 raw 中);查看一个应用的最新版本或指定的版本,它的定义在 raw 中。
glac.installations().create(app), create(app, new InstallationOptions().name(..).version(..).region(..).parameters(..).requires(name, installation).noWait()) 安装它,并等它就绪(noWait:立即返回)。安装失败时抛出类型和错误码都为 installation_failed 的 GlacException,附带错误信息。
glac.installations().list(), list(app), get(idOrName) 项目的安装,最新的在前;查看一个。
installation.update(new InstallationUpdate().version(..).parameters(..)), delete(), refresh(), await(), await(Duration) 换一个版本,或以合并补丁的方式修改参数(null 把参数恢复为默认值);连同它的所有组成部分一起删除(status():deleted 或 deleting);await(Duration) 以 wait_timeout 结束时,安装仍会继续。
installation.id(), name(), status(), info() 按 API 给出的样子:info() 包含 outputs、secretOutputs、parameters、apps、volumes、buckets、requires、failure。

托管应用属于控制平面。

机密与变量

  • 机密。glac.secrets().put("DB_URL", url, List.of("db.example.com")) 按名称保存一个值,以及它可以发往的主机;update(name, hosts) 只修改主机,list() 给出名称(从不给出值),delete(name) 删除一个。应用的环境变量按名称引用机密 (new AppOptions().secret("VAR", "DB_URL")),非公开的镜像 (imagePullSecret) 和沙箱的出站规则也一样。会把机密发往别处的调用会被拒绝,返回 403 secret_host_not_allowed。
  • 共享变量。glac.variables().put(name, value)、list()、delete(name):服务的 env 引用的值 ("${{ shared.REGION }}"),作用于设置之后创建的版本。属于控制平面。

项目、账户与账单

平台的调用是客户端自己的方法:glac.project() 和 glac.events(EventsOptions)(项目及其事件的一页)、glac.account() 和 glac.updateAccount(AccountOptions)(本月的流量、出站设置、地址、通知的语言、账单地址和税号)、glac.usage(ListOptions) 和 glac.usageReport(UsageOptions)(用量)、glac.billing() 和 glac.updateBilling(BillingOptions)(套餐、计量项和额度、订阅)、glac.balance()、glac.updateBalance(BalanceOptions)、glac.topUp(usd) 和 glac.topUp(usd, TopUpOptions)(点数、自动充值、充值)、glac.charges() 和 glac.ledger()。账单和项目属于控制平面;独立运行的节点会以它自己的 404 响应这些调用。

事件的数据。控制平面的每种事件,在 Event 中都有一个对应其数据的 record,以描述中该事件的 schema 命名,它的 of(event) 读取对应类型的事件,其他类型的事件返回 null (Event.Database.of(e))。数据库的事件:Event.Database(database.created,附带它的 region 和 PostgreSQL version;database.deleted,附带 final_snapshot_id,即保留七天的云盘快照)、Event.DatabaseBranch(database.branch_created,附带它的 parent;database.branch_deleted,附带它的 reason)、Event.DatabaseRestored(database.restored:时刻 at,以及 old_branch_id,即保存恢复之前状态的分支)、Event.DatabaseArchive(database.archive_failing:since、连续失败的分段数 failures、archived_until,即在此期间恢复能追溯多远,以及对象存储的 error;database.archive_recovered:recovered_at)和 Event.DatabaseGap。其他事件也一样:沙箱的(Event.SandboxSlept、Event.SandboxWoke、Event.SandboxMoved、Event.SandboxDeleted 等)、应用及其键的(Event.App、Event.AppFailed、Event.KeyCreated 等)、项目的部署和密钥、备份、域名、导出目标和云盘快照的(Event.ProjectDeployed、Event.Backup、Event.Domain、Event.Destination、Event.DriveSnapshot 等),以及账户的套餐、充值、退款、撤回、余额、点数、存储、出站流量上限、支出上限、冻结和欠款的(Event.SubscriptionPeriod、Event.TopupCollected、Event.WithdrawalReceived、Event.Balance、Event.InDebt 等)。某类事件并非都有的字段,或为 null 的字段,取值为 null(这类数字字段是 Long 或 Double)。

Webhook、API 密钥、支出上限、项目。glac.webhooks().create(url, new WebhookOptions()…) 把项目的事件发到你的地址(为每次投递签名的 secret() 只出现在这次响应中);还有 get、list、update、delete。glac.keys().create(name, scopes, expiresAt) 创建一个 API 密钥(密钥本身 key() 只出现在这次响应中);还有 get、list、delete。glac.limits() 和 glac.updateLimits(50.0) 是项目的支出上限和本月已支出的金额(null 去掉上限),sbx.limits()、sbx.updateLimits(…) 则是某个沙箱自己的。glac.requestLimit(limit, wanted, why) 申请提高账户的某项可应要求提高的限制(按名称指定的上限、"volumes" 或 "volume_bytes";后两者中的大小以字节为单位;同一项限制已有申请在等待时返回 409 limit_request_pending),glac.limitRequests(new ListOptions()) 和 glac.limitRequest(id) 读取申请及运营方的答复(一个 LimitRequest:status 为 pending、granted 或 refused,granted、answer)。Event.InstanceRestarted.of(event)、Event.InstanceUpgrade.of(instance.upgraded、.reverted、.upgrade_failed)和 Event.LimitRequestAnswer.of 读取这些事件的数据;沙箱(以及实例)的 info().startCommand() 说明它的启动命令如何运行、在机器中被重新启动了多少次,以及上一次是如何结束的。glac.updateProject(30) 设置沙箱休眠多少天后被删除(null:永不删除)。属于控制平面。Webhook 和 API 密钥需要有 admin 权限范围的密钥,设置项目的上限和 updateProject 也一样;设置沙箱的上限需要 write。

日志导出目标、审计日志、计费文件。glac.destinations() 是项目中应用的日志(以及指标和请求的追踪:.signals("logs", "traces"))导出的地方(OpenTelemetry 端点):create(name, new DestinationOptions().endpoint(u).header(name, value)) 创建时会立即用一条记录测试 (status().connected()),还有 get、list、update、delete、test(立即发送一条记录);请求头的值是凭据,只在创建或修改导出目标时发送,不会出现在任何响应中(返回的内容只有请求头的名称)。glac.audit(new ListOptions().limit(n)) 是项目审计日志的一页,最新的在前(谁、什么方法、什么路径、什么状态):每一次不是读取的调用(被拒绝的也算),以及运营方对应用日志的每一次读取。glac.statement("2026-09") 是某个月的对账单(null 表示本月),其中每个数据库单独一行(databases():它这个月的用量,其中历史和分层数据单列,以及按标价折合多少),glac.invoices() 是 Stripe 上的发票(附 PDF 链接),glac.notices() 是发给账户的邮件,glac.withdrawals(new ListOptions()) 是现在可以撤回的购买和已做的撤回(欧洲经济区或英国的消费者可以在 14 天内撤回一笔购买),glac.withdraw(chargeId, name, email) 是买方本人的撤回声明,立即受理。属于控制平面。创建、修改、测试和删除导出目标、读取审计日志以及撤回,需要有 admin 权限范围的密钥;其他调用只需要有读取权限的密钥。

账户的 email() 是它的通知发往的地址,用 new AccountOptions().email(address) 设置:emailVerified() 说明它是否已验证(只给已验证的地址发邮件:未验证的地址会先收到一个链接),emailUndeliverable()(一个 Account.EmailUndeliverable:why、since、detail)说明发往它的邮件被退回了;在此期间通知会暂缓发送(通知发往哪里)。账户的 language() 是它的通知使用的语言(一个 Account.Language:code 为 “en”、“de” 或 “fr”,source 为 “account”、“billing_address” 或 “default”);new AccountOptions().language("de") 选择语言,noLanguage() 撤销选择,重新由账单地址决定。撤回的回执和购买的确认用这种语言撰写;其他所有通知都是英文。

账户的 keeps()(一个 Account.Keeps,在控制平面后面;在独立运行的节点上为 null)是密钥所属项目在云盘和存储卷上的保有量,按创建时所受限制的数字给出(403 drive_limit_reached、volume_limit_reached),每项都附带项目已有的量:volumes(一个 Account.KeepsCount:limit、used,以及 source,为 “plan” 或 “operator”)、volumeBytes(一个 Account.KeepsSize:单个的大小上限,以及它的 source)、volumesBytes(一个 Account.KeepsTotal:全部合计的大小上限,limit 为 null 表示不限,以及 used),以及 unsizedBytes,即不指定大小时创建的大小。账户自己的数字 (“operator”) 用 glac.requestLimit("volumes", …) 或 "volume_bytes" 申请。

立即购买 (startNow)。欧洲经济区或英国的消费者(没有已验证或正在验证的税号的账户)可以在 14 天内撤回一笔购买,所购内容只有在其本人明确要求时才会立即提供。startNow() 就带有这一要求:glac.topUp(20, new TopUpOptions().startNow())、new BillingOptions().plan("paid").startNow()(从免费版购买付费版)和 new BalanceOptions().autoReload(true).startNow()(自动充值保持开启期间的每一次自动充值),措辞与控制台在勾选框旁显示的相同(GET /v1/billing/withdrawals 中也有,见 right.start_now):

我要求在可以撤回这笔购买的 14 天内立即提供所购内容。如果我撤回,我需要为此前已提供的部分付费。我理解,一次充值的点数全部用完后,即视为已全部提供,我将不能再撤回。

服务立即开始;在 14 天内,买方仍可撤回,并为此前已提供的部分付费;一旦全部提供(一次充值的点数全部用完),撤回权即丧失。只有在购买者本人提出这一要求时才加上它:SDK 不会自行发送它。没有它时,这类账户的购买会被拒绝:抛出一个 GlacException,状态为 400,错误码为 start_now_required,param() 为 start_now,错误信息会指明账户所在的国家并引用这一要求的原文,什么也不会买。开启了自动充值却没有这一要求的账户不会自动充值,并且每月会收到一次通知(auto_reload.no_request 事件,用 Event.AutoReloadNoRequest.of 读取;没有绑定银行卡的账户则是 auto_reload.no_card 和 Event.AutoReloadNoCard)。

错误

非零退出码是一个结果:读取 res.exitCode() 即可。API 的错误会抛出 GlacException,带有 status()、type()、code()、param()、retryable()、requestId() 和 message(),以及指向错误页面上对应错误码的 docsUrl();没有得到响应的调用也一样(status() 为 0,type() 为 unavailable 或 timeout)。无论调用属于哪个产品,它都是平台的错误。与费用有关的错误(402 account_in_debt、403 trust_limit_exceeded、409 no_card、400 start_now_required 等)带有控制平面自己的错误信息,说明怎样解除。

值得了解的行为

  • 休眠的沙箱。不会有调用因为沙箱在休眠或正在启动而失败:它会等待,然后继续。res.wake() 说明 run 为唤醒等待了多久;客户端上的 onResponse 能看到每个调用的这一信息。
  • 重试。没有响应,或出现 retryable 错误时,会重试两次 (retries),间隔从 0.2 秒开始翻倍。每个 POST 都带有 Idempotency-Key;idempotencyKey(key) 让重启后的程序能拿回它之前创建的东西。
  • 超时。timeout(默认 15 分钟)是等待 API 的时长;RunOptions.timeout 是命令自己的时限。
  • 线程。Glac、它的命名空间、Sandbox、Command 和文件对象都可以在多个线程中使用。SDK 会启动一个自己的守护线程,用来监听事件流。
导航

输入以搜索……

↑↓ 切换↵ 选择Esc 关闭