From 65a11fd929d9e743c0320664c9599111c6425392 Mon Sep 17 00:00:00 2001 From: jiaoliao Date: Tue, 3 Feb 2026 10:34:01 +0000 Subject: [PATCH 1/3] add release note 120 --- .../version-1.2.x/Release Notes/v1.2.0.md | 54 ++++++++++++++++++- .../version-1.2.x/Release Notes/v1.2.0.md | 52 ++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.2.x/Release Notes/v1.2.0.md b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.2.x/Release Notes/v1.2.0.md index 15b9fc92b7..8d0cc77106 100644 --- a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.2.x/Release Notes/v1.2.0.md +++ b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.2.x/Release Notes/v1.2.0.md @@ -51,4 +51,56 @@ - 演示 `rock model-service start --type proxy --proxy-base-url` 用法 -## Admin \ No newline at end of file +## Admin + +### 新功能 + +#### 任务调度器(Task Scheduler) +一个灵活的任务调度系统,用于管理 Ray Worker 节点上的周期性维护任务。 + +**内置任务:** +- **ImageCleanupTask**:使用 [docuum](https://github.com/stepchowfun/docuum) 进行 Docker 镜像清理,支持可配置的磁盘阈值 + +**配置示例:** +```yaml +scheduler: + enabled: true + worker_cache_ttl: 3600 + tasks: + - task_class: "rock.admin.scheduler.tasks.image_cleanup_task.ImageCleanupTask" + enabled: true + interval_seconds: 3600 + params: + threshold: "1T" +``` + +**可扩展性:** +通过继承 `BaseTask` 并实现 `run_action(runtime: RemoteSandboxRuntime)` 方法来创建自定义任务。 + +#### Entrypoints + +- **新增**: 批量查询沙箱状态 API (`POST /sandboxes/batch`) + - 在单个请求中高效检索多个沙箱的状态信息 + - 接受包含 `sandbox_ids` 列表的 `BatchSandboxStatusRequest` 请求 + - 返回包含所有请求沙箱状态详情的 `BatchSandboxStatusResponse` 响应 + - 支持可配置的最大数量限制(默认值来自 `batch_get_status_max_count` 配置) + +- **新增**: 沙箱列表和过滤 API (`GET /sandboxes`) + - 使用灵活的查询参数查询和过滤沙箱 + - 通过 `page` 和 `page_size` 参数支持分页 + - 返回包含条目列表、总数和 `has_more` 标识的 `SandboxListResponse` 响应 + - 支持按沙箱属性过滤(例如:部署类型、状态、自定义元数据等) + - 最大页面大小可通过 `batch_get_status_max_count` 配置 + +#### Sandbox + +- 修改 Sandbox 日志格式:时间戳使用 ISO 8601 格式,默认时区为 Asia/Shanghai(亚洲/上海) +- 丰富 SandboxInfo:添加 create_time、start_time、stop_time +- 添加计费日志:当 sandbox 关闭时记录计费日志 + +--- + +### 增强 + +#### 性能优化 +- 实现 get_status 接口逻辑与 Ray 的解耦:将 API 响应耗时从 1s 以上显著降低至 100ms 级别;支持新旧逻辑的动态在线切换。 diff --git a/docs/versioned_docs/version-1.2.x/Release Notes/v1.2.0.md b/docs/versioned_docs/version-1.2.x/Release Notes/v1.2.0.md index 8c09e2b0cb..a403d35bf1 100644 --- a/docs/versioned_docs/version-1.2.x/Release Notes/v1.2.0.md +++ b/docs/versioned_docs/version-1.2.x/Release Notes/v1.2.0.md @@ -52,3 +52,55 @@ Practical examples demonstrating ROCK Agent integration with various AI framewor ## Admin + +### New Features + +#### Task Scheduler +A flexible task scheduling system for managing periodic maintenance tasks across Ray workers. + +**Built-in Tasks:** +- **ImageCleanupTask**: Docker image cleanup using [docuum](https://github.com/stepchowfun/docuum) with configurable disk threshold + +**Configuration Example:** +```yaml +scheduler: + enabled: true + worker_cache_ttl: 3600 + tasks: + - task_class: "rock.admin.scheduler.tasks.image_cleanup_task.ImageCleanupTask" + enabled: true + interval_seconds: 3600 + params: + threshold: "1T" +``` + +**Extensibility:** +Create custom tasks by extending `BaseTask` and implementing the `run_action(runtime: RemoteSandboxRuntime)` method. + +#### Entrypoints + +- **NEW**: Batch sandbox status query API (`POST /sandboxes/batch`) + - Efficiently retrieve status information for multiple sandboxes in a single request + - Accepts `BatchSandboxStatusRequest` with `sandbox_ids` list + - Returns `BatchSandboxStatusResponse` containing status details for all requested sandboxes + - Supports up to configurable maximum count (default from `batch_get_status_max_count`) + +- **NEW**: Sandbox listing and filtering API (`GET /sandboxes`) + - Query and filter sandboxes with flexible query parameters + - Supports pagination via `page` and `page_size` parameters + - Returns `SandboxListResponse` with items, total count, and `has_more` indicator + - Enables filtering by sandbox attributes (e.g., deployment type, status, custom metadata) + - Maximum page size configurable via `batch_get_status_max_count` + +#### Sandbox + +- Modify Sandbox log format: use iso 8601 format for timestamp, default time zone is Asia/Shanghai +- Enrich SandboxInfo: add create_time, start_time, stop_time for metrics +- Add Billing log when sandbox is closed + +--- + +### Enhancements + +#### Performance Optimizations +- Decouple the `get_status` API logic from Ray, reducing API latency from 1s+ to 100ms+; support dynamically toggling between the new and legacy logic. From 91575824ac176639d532db8e40dd635ed0a8bb67 Mon Sep 17 00:00:00 2001 From: jiaoliao Date: Tue, 3 Feb 2026 10:55:42 +0000 Subject: [PATCH 2/3] Revert "add release note 120" This reverts commit 65a11fd929d9e743c0320664c9599111c6425392. --- .../version-1.2.x/Release Notes/v1.2.0.md | 54 +------------------ .../version-1.2.x/Release Notes/v1.2.0.md | 52 ------------------ 2 files changed, 1 insertion(+), 105 deletions(-) diff --git a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.2.x/Release Notes/v1.2.0.md b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.2.x/Release Notes/v1.2.0.md index 8d0cc77106..15b9fc92b7 100644 --- a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.2.x/Release Notes/v1.2.0.md +++ b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.2.x/Release Notes/v1.2.0.md @@ -51,56 +51,4 @@ - 演示 `rock model-service start --type proxy --proxy-base-url` 用法 -## Admin - -### 新功能 - -#### 任务调度器(Task Scheduler) -一个灵活的任务调度系统,用于管理 Ray Worker 节点上的周期性维护任务。 - -**内置任务:** -- **ImageCleanupTask**:使用 [docuum](https://github.com/stepchowfun/docuum) 进行 Docker 镜像清理,支持可配置的磁盘阈值 - -**配置示例:** -```yaml -scheduler: - enabled: true - worker_cache_ttl: 3600 - tasks: - - task_class: "rock.admin.scheduler.tasks.image_cleanup_task.ImageCleanupTask" - enabled: true - interval_seconds: 3600 - params: - threshold: "1T" -``` - -**可扩展性:** -通过继承 `BaseTask` 并实现 `run_action(runtime: RemoteSandboxRuntime)` 方法来创建自定义任务。 - -#### Entrypoints - -- **新增**: 批量查询沙箱状态 API (`POST /sandboxes/batch`) - - 在单个请求中高效检索多个沙箱的状态信息 - - 接受包含 `sandbox_ids` 列表的 `BatchSandboxStatusRequest` 请求 - - 返回包含所有请求沙箱状态详情的 `BatchSandboxStatusResponse` 响应 - - 支持可配置的最大数量限制(默认值来自 `batch_get_status_max_count` 配置) - -- **新增**: 沙箱列表和过滤 API (`GET /sandboxes`) - - 使用灵活的查询参数查询和过滤沙箱 - - 通过 `page` 和 `page_size` 参数支持分页 - - 返回包含条目列表、总数和 `has_more` 标识的 `SandboxListResponse` 响应 - - 支持按沙箱属性过滤(例如:部署类型、状态、自定义元数据等) - - 最大页面大小可通过 `batch_get_status_max_count` 配置 - -#### Sandbox - -- 修改 Sandbox 日志格式:时间戳使用 ISO 8601 格式,默认时区为 Asia/Shanghai(亚洲/上海) -- 丰富 SandboxInfo:添加 create_time、start_time、stop_time -- 添加计费日志:当 sandbox 关闭时记录计费日志 - ---- - -### 增强 - -#### 性能优化 -- 实现 get_status 接口逻辑与 Ray 的解耦:将 API 响应耗时从 1s 以上显著降低至 100ms 级别;支持新旧逻辑的动态在线切换。 +## Admin \ No newline at end of file diff --git a/docs/versioned_docs/version-1.2.x/Release Notes/v1.2.0.md b/docs/versioned_docs/version-1.2.x/Release Notes/v1.2.0.md index a403d35bf1..8c09e2b0cb 100644 --- a/docs/versioned_docs/version-1.2.x/Release Notes/v1.2.0.md +++ b/docs/versioned_docs/version-1.2.x/Release Notes/v1.2.0.md @@ -52,55 +52,3 @@ Practical examples demonstrating ROCK Agent integration with various AI framewor ## Admin - -### New Features - -#### Task Scheduler -A flexible task scheduling system for managing periodic maintenance tasks across Ray workers. - -**Built-in Tasks:** -- **ImageCleanupTask**: Docker image cleanup using [docuum](https://github.com/stepchowfun/docuum) with configurable disk threshold - -**Configuration Example:** -```yaml -scheduler: - enabled: true - worker_cache_ttl: 3600 - tasks: - - task_class: "rock.admin.scheduler.tasks.image_cleanup_task.ImageCleanupTask" - enabled: true - interval_seconds: 3600 - params: - threshold: "1T" -``` - -**Extensibility:** -Create custom tasks by extending `BaseTask` and implementing the `run_action(runtime: RemoteSandboxRuntime)` method. - -#### Entrypoints - -- **NEW**: Batch sandbox status query API (`POST /sandboxes/batch`) - - Efficiently retrieve status information for multiple sandboxes in a single request - - Accepts `BatchSandboxStatusRequest` with `sandbox_ids` list - - Returns `BatchSandboxStatusResponse` containing status details for all requested sandboxes - - Supports up to configurable maximum count (default from `batch_get_status_max_count`) - -- **NEW**: Sandbox listing and filtering API (`GET /sandboxes`) - - Query and filter sandboxes with flexible query parameters - - Supports pagination via `page` and `page_size` parameters - - Returns `SandboxListResponse` with items, total count, and `has_more` indicator - - Enables filtering by sandbox attributes (e.g., deployment type, status, custom metadata) - - Maximum page size configurable via `batch_get_status_max_count` - -#### Sandbox - -- Modify Sandbox log format: use iso 8601 format for timestamp, default time zone is Asia/Shanghai -- Enrich SandboxInfo: add create_time, start_time, stop_time for metrics -- Add Billing log when sandbox is closed - ---- - -### Enhancements - -#### Performance Optimizations -- Decouple the `get_status` API logic from Ray, reducing API latency from 1s+ to 100ms+; support dynamically toggling between the new and legacy logic. From 6f5ba2215924d08e7ff7d49449d06501fc8bf301 Mon Sep 17 00:00:00 2001 From: jiaoliao Date: Wed, 27 May 2026 11:11:33 +0000 Subject: [PATCH 3/3] add benchmark doc & copy scheduler guide --- .../User Guides/sandbox_create_benchmark.md | 43 +++ .../version-1.8.x/User Guides/scheduler.md | 283 ++++++++++++++++++ .../sandbox_create_latency_distribution.png | Bin 0 -> 100042 bytes .../User Guides/sandbox_create_benchmark.md | 43 +++ .../version-1.8.x/User Guides/scheduler.md | 283 ++++++++++++++++++ 5 files changed, 652 insertions(+) create mode 100644 docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.8.x/User Guides/sandbox_create_benchmark.md create mode 100644 docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.8.x/User Guides/scheduler.md create mode 100644 docs/static/img/sandbox_create_latency_distribution.png create mode 100644 docs/versioned_docs/version-1.8.x/User Guides/sandbox_create_benchmark.md create mode 100644 docs/versioned_docs/version-1.8.x/User Guides/scheduler.md diff --git a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.8.x/User Guides/sandbox_create_benchmark.md b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.8.x/User Guides/sandbox_create_benchmark.md new file mode 100644 index 0000000000..a5bbb0f361 --- /dev/null +++ b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.8.x/User Guides/sandbox_create_benchmark.md @@ -0,0 +1,43 @@ +# ROCK Sandbox 大规模并发创建压测报告 + +## 1. 测试背景 + +在 Agentic 强化学习训练以及大规模 Agent Rollout 场景中,**单个训练 step 往往需要同时拉起成千上万个 Sandbox**,因此 Sandbox 的并发创建吞吐与延迟直接决定了整体训练效率。 + +本报告分别在 1000 / 2000 / 4000 / 8000 / 16000 并发规模下完成 Sandbox 批量创建压测,验证 ROCK 在大规模并发下的稳定性与延迟表现。 + +## 2. 测试范围与口径 + +- **被测对象**:ROCK Sandbox 的创建链路。 +- **统计指标**:单个 Sandbox 从发起创建请求到 Sandbox 处于存活(可用)状态的端到端耗时(秒)。 +- **并发规模**:1000、2000、4000、8000、16000 个 Sandbox 同时发起创建。 +- **并发模型**:采用 **多机分布式 + 纯多进程** 的方式驱动并发——由多台机器同时发压,每台机器内部以独立 OS 进程承载每个 Sandbox 创建任务,避免单进程内 GIL、事件循环、TLS 握手等成为客户端侧瓶颈,使数据真实反映服务端的处理能力。 + +## 3. 测试结果 + +### 3.1 Sandbox 创建耗时统计 + +| 并发规模 | 样本数 | 成功率 | 最小值 | 最大值 | 平均值 | P50 | P95 | P99 | +|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| **1000** | 1000 | 100% | 0.47s | 9.84s | 4.72s | 4.93s | 6.89s | 8.69s | +| **2000** | 2000 | 100% | 0.64s | 11.20s | 4.90s | 4.92s | 9.76s | 10.53s | +| **4000** | 4000 | 100% | 0.93s | 15.51s | 7.16s | 7.05s | 11.26s | 13.34s | +| **8000** | 8000 | 100% | 3.85s | 33.99s | 18.06s | 17.86s | 30.09s | 32.03s | +| **16000** | 16000 | 100% | 2.66s | 63.84s | 37.17s | 39.98s | 56.68s | 60.11s | + +> 时间单位均为秒(s)。所有规模下成功率均为 **100%**(0 失败)。 +> +> 说明:随着并发规模的增大,为保护服务端稳定性,ROCK 会在控制面侧进行限流,因此耗时随并发上升而有所增加。 + +![Sandbox 创建耗时分布](../../../../../static/img/sandbox_create_latency_distribution.png) + +## 4. 结论 + +ROCK 在 1000 至 16000 并发规模下完成的 Sandbox 批量创建压测取得了以下结果: + +1. **100% 成功率**,验证了 ROCK 在大规模并发下的可靠性。 +2. **小规模并发(≤2000)几乎无排队**,P50 稳定在 5s 以内,可以满足绝大多数实时性敏感的训练 / 评估场景。 +3. **大规模并发(≥4000)延迟随并发数近似线性增长**,行为可预测,便于按 Sandbox 池规模做容量规划。 +4. **16000 并发场景下 P99 仍可控制在 60s 量级**,足以支撑超大规模 Agent Rollout 与并行 RL Step 等任务。 + +ROCK 已经具备承载 **万级并发 Sandbox 创建** 的能力,为大规模 Agentic RL 训练提供了坚实的环境基础设施。 diff --git a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.8.x/User Guides/scheduler.md b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.8.x/User Guides/scheduler.md new file mode 100644 index 0000000000..88662edd51 --- /dev/null +++ b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/version-1.8.x/User Guides/scheduler.md @@ -0,0 +1,283 @@ +--- +sidebar_position: 5 +--- + +# 任务调度器(Scheduler) + +ROCK 调度器是内嵌于 `admin` 服务中的周期性任务框架。它会按可配置的时间间隔,把后台维护任务(镜像清理、文件清理、容器清理、镜像预拉取、自定义任务……)分发到所有存活的 Ray worker 上,从而在无人工干预的情况下保持 worker 节点健康。 + +本文介绍如何启用调度器、配置内置任务、编写自定义任务,以及如何观测运行状态。 + +## 1. 工作原理 + +- 调度器以独立的守护线程(`SchedulerThread`)运行在 `admin` 进程内,使用自己的 `asyncio` 事件循环。 +- 任务通过 [APScheduler](https://apscheduler.readthedocs.io/) 以固定间隔(`interval_seconds`)触发。 +- 每次触发时,调度器会先获取存活 Ray worker 列表(由 `worker_cache_ttl` 秒级缓存),然后并发地把任务下发到每个 worker(默认并发度:50)。 +- **下发动作通过 worker 上的 rocklet 服务以 HTTP 方式完成**:admin 端构造 `RemoteSandboxRuntime(host=worker_ip, port=Port.PROXY)`(参见 `rock.deployments.constants.Port`),并调用 `runtime.execute / read_file / write_file`。**因此每个 worker 都必须运行 `rocklet` 服务并在 `Port.PROXY` 上可达**,否则调度器无法下发命令、也无法在 worker 上读写状态文件。 +- 每个任务都继承自 `rock.admin.scheduler.task_base.BaseTask`,并必须实现 `run_action(runtime: RemoteSandboxRuntime)` —— 单个 worker 上的实际执行逻辑。 +- 每个 worker 的执行状态会被持久化到 `ROCK_SCHEDULER_STATUS_DIR`(默认 `/data/scheduler_status`)目录下的 JSON 文件中;每次执行结束后还会写入聚合报告 `/_run_report.json`。 +- 当配置了 Nacos 配置源时,调度器会订阅配置变更,并按 diff 应用:仅 hash 发生变化的任务被重新安装,被删除的任务会同步从所有 worker 上清理。 + +### 前置条件 + +启用调度器之前,请确认每个 Ray worker 满足以下条件: + +| 条件 | 原因 | +|------|------| +| worker 上正在运行 `rocklet` 进程 | 调度器通过 rocklet HTTP 接口下发每一个任务;若 rocklet 不存在,`runtime.execute` 调用会超时。 | +| admin 可访问 rocklet 监听端口 | 调度器固定使用 `Port.PROXY`(定义于 `rock.deployments.constants.Port`)作为下发目标,请确认防火墙 / 安全组未阻断该端口。 | +| `ROCK_SCHEDULER_STATUS_DIR` 在 worker 内可写 | 任务会在该目录下读写 `_status.json`,用于幂等控制和 PID 跟踪。 | +| 任务依赖的工具在 worker 上可用 | 例如清理 / 拉取类任务需要 `docker`;`ImageCleanupTask` 首次运行会通过 `curl` 联网安装 `docuum`。 | + +rocklet 服务由 worker 标准启动脚本(`docker_run.sh`、`docker_run_with_uv.sh`、`docker_run_with_pip.sh`)自动拉起,通常等价于 `rocklet --port `。如果你使用了自定义 entrypoint 来启动 worker,请确保等价命令被执行。具体的运行时类型与 rocklet 启动方式可参考 [Configuration](./configuration.md)。 + +### 幂等性 + +每个任务都需要声明自己的幂等模式,该模式直接影响重复触发时的行为: + +| 模式 | 行为 | +|------|------| +| `IDEMPOTENT` | 每次 tick 都会执行,可安全重复(例如 `docker pull`、`find -exec rm`)。 | +| `NON_IDEMPOTENT` | 任务会拉起一个后台守护进程(例如 `docuum`)。调度器会读取上一次的状态文件,检查记录的 PID 是否仍存活,若仍在运行则跳过本次启动。当任务从配置中移除时,调度器会通过 `pkill` 杀掉该进程。 | + +## 2. 启用调度器 + +调度器配置位于 ROCK admin YAML 顶层 `scheduler:` 字段下(例如 `rock-conf/rock-local.yml`、`rock-conf/rock-dev.yml`)。 + +```yaml +scheduler: + enabled: true # 总开关 + worker_cache_ttl: 43200 # Worker IP 缓存 TTL(秒) + tasks: + # ... 任务列表,详见下文 +``` + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `enabled` | bool | `false` | 总开关。设为 `false` 时所有任务会被卸载,且不再触发。 | +| `worker_cache_ttl` | int | `3600` | 存活 worker IP 列表缓存时长(秒);超过该时长后会从 `ray.nodes()` 重新获取。 | +| `tasks` | list | `[]` | `TaskConfig` 列表,详见 [第 4 节](#4-任务配置schema)。 | + +### 相关环境变量 + +| 变量 | 默认值 | 用途 | +|------|--------|------| +| `ROCK_SCHEDULER_STATUS_DIR` | `/data/scheduler_status` | worker 上写入任务状态 JSON 与执行报告的目录。 | +| `ROCK_LOGGING_PATH` | (未设置) | 设置后,调度器拉起的守护进程(docuum、container_cleanup、image_pull)会将 stdout/stderr 重定向到 `/.log`。 | +| `ROCK_DOCUUM_INSTALL_URL` | `https://raw.githubusercontent.com/stepchowfun/docuum/main/install.sh` | `ImageCleanupTask` 按需拉取 `docuum` 安装脚本的 URL。 | + +## 3. 内置任务 + +ROCK 在 `rock.admin.scheduler.tasks` 下提供了 4 个内置任务,通过把 `task_class` 设置为对应的全限定类路径即可注册。 + +### 3.1 ImageCleanupTask + +在每个 worker 上运行 [`docuum`](https://github.com/stepchowfun/docuum),当磁盘占用超过阈值时按 LRU 策略淘汰镜像。**非幂等** —— `docuum` 是常驻守护进程;调度器会跟踪其 PID,只要进程仍存活就跳过重复拉起。 + +```yaml +- task_class: rock.admin.scheduler.tasks.image_cleanup_task.ImageCleanupTask + enabled: true + interval_seconds: 43200 # 每 12 小时检查一次守护进程 + params: + disk_threshold: "70%" # 磁盘占用超过 70% 时触发淘汰 + image_whitelist: # 匹配 repository:tag 的 glob 模式,白名单内的镜像不会被淘汰 + - "python:3.11" + - "my-registry.example.com/base/*" +``` + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `disk_threshold` | str | `"1T"` | 传给 `docuum --threshold` 的磁盘阈值,支持容量(`100G`、`1T`)或百分比(`70%`)。 | +| `image_whitelist` | list[str] | `[]` | 透传给 `docuum --keep` 的 glob 模式列表。 | + +### 3.2 FileCleanupTask + +遍历配置的目录,删除超过 `max_age_mins` 或大于 `max_file_size` 的文件,然后清理留下的空目录。**幂等**。 + +```yaml +- task_class: rock.admin.scheduler.tasks.file_cleanup_task.FileCleanupTask + enabled: true + interval_seconds: 86400 # 每天执行一次 + params: + target_dirs: + # 字符串形式 —— 不配置排除项 + - "/data/service_status" + # 对象形式 —— 配置该目录独有的排除项 + - path: "/data/logs" + exclude_files: # 支持纯文件名 / 相对路径 / 绝对路径 + - "docuum.log" + - "./rocklet.log" + - "./access.log" + exclude_dirs: + - ".cache" + max_age_mins: 10080 # 7 天,超出此时间的文件会被删除 + max_file_size: "1G" # 大于此大小的文件会被删除(支持 K/M/G/T) +``` + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `target_dirs` | list | `[]` | 每个条目是一个字符串(只填路径)或 `{path, exclude_files, exclude_dirs}`。 | +| `max_age_mins` | int | `10080` | mtime 早于该分钟数的文件会被删除。 | +| `max_file_size` | str | `"1G"` | 大于该阈值的文件会被删除,支持 `K/M/G/T` 单位。 | + +删除条件为 `(-mmin +max_age_mins) OR (-size +max_file_size)`。文件清理后,会再用 `find -depth -type d -empty -delete` 清理留下的空目录(同样遵循 `exclude_dirs` 配置)。 + +### 3.3 ContainerCleanupTask + +删除停止时间超过指定时长的 Docker 容器,避免 worker 上的容器列表无限增长。**幂等**。 + +```yaml +- task_class: rock.admin.scheduler.tasks.container_cleanup_task.ContainerCleanupTask + enabled: true + interval_seconds: 86400 + params: + max_age_hours: 72 # 删除超过 72 小时的 exited 容器 +``` + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `max_age_hours` | int | `24` | 已退出容器的最大保留时间(以 `FinishedAt` 起算的小时数);超出后被 `docker rm`。 | + +每次执行还会顺带清理处于 `created` 状态(从未启动)的容器。 + +### 3.4 ImagePullTask + +在每个 worker 上预拉取一组 Docker 镜像,并可选地先登录私有仓库,以降低沙箱冷启动延迟。**幂等**(若镜像已是最新,`docker pull` 等同于空操作)。 + +```yaml +- task_class: rock.admin.scheduler.tasks.image_pull_task.ImagePullTask + enabled: true + interval_seconds: 21600 # 每 6 小时刷新一次 + params: + images: + # 字符串形式 —— 公开镜像,无需鉴权 + - "python:3.11" + # 对象形式 —— 私有镜像,需要登录 + - image: "my-registry.example.com/chatos/python:313" + registry_username: "myuser" + registry_password: "bXlwYXNzd29yZA==" # base64 编码 +``` + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `images` | list | `[]` | 每个条目是一个镜像字符串或 `{image, registry_username, registry_password}`。 | + +`registry_password` 必须是 base64 编码,worker 端会先解码再通过 `docker login --password-stdin` 登录。仓库地址会从镜像名称中解析,因此每个镜像可以指向不同的仓库。 + +## 4. 任务配置 Schema + +`scheduler.tasks` 下的每一项都会被解析为 `rock.config.TaskConfig`: + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `task_class` | str | `""` | Python 类的全限定路径,**必填**。 | +| `enabled` | bool | `true` | 设为 `false` 的任务在加载阶段会被跳过,在 reload 阶段会被卸载。 | +| `interval_seconds` | int | `3600` | APScheduler `interval` 间隔(秒)。 | +| `params` | dict | `{}` | 任务自定义参数,会在 `from_config()` 中被消费。 | + +只要某个任务条目中任何字段发生变化,调度器就会卸载旧任务(非幂等任务还会清理 worker 上的守护进程与状态文件)再安装新任务,**整个过程不需要重启 admin 进程**。 + +## 5. 编写自定义任务 + +任何位于 Python 路径下、继承自 `BaseTask` 的类都可以注册为调度任务。最小契约示例如下: + +```python +# my_pkg/my_tasks/disk_report_task.py +from rock.admin.proto.request import SandboxCommand as Command +from rock.admin.scheduler.task_base import BaseTask, IdempotencyType, TaskStatusEnum +from rock.sandbox.remote_sandbox import RemoteSandboxRuntime + + +class DiskReportTask(BaseTask): + """记录每个 worker 的 `df -h` 输出。""" + + def __init__(self, interval_seconds: int = 3600, mount_point: str = "/"): + super().__init__( + type="disk_report", # 同时作为 APScheduler job id 与状态文件名前缀 + interval_seconds=interval_seconds, + idempotency=IdempotencyType.IDEMPOTENT, + ) + self.mount_point = mount_point + + @classmethod + def from_config(cls, task_config) -> "DiskReportTask": + return cls( + interval_seconds=task_config.interval_seconds, + mount_point=task_config.params.get("mount_point", "/"), + ) + + async def run_action(self, runtime: RemoteSandboxRuntime) -> dict: + result = await runtime.execute( + Command(command=f"df -h {self.mount_point}", shell=True), + ) + return { + "status": TaskStatusEnum.SUCCESS, + "exit_code": result.exit_code, + "stdout": result.stdout, + } +``` + +随后在 YAML 中注册: + +```yaml +scheduler: + enabled: true + tasks: + - task_class: my_pkg.my_tasks.disk_report_task.DiskReportTask + enabled: true + interval_seconds: 600 + params: + mount_point: "/data" +``` + +### 自定义任务编写要点 + +- **`super().__init__()` 中的 `type` 必须全局唯一**:它会同时被用作 APScheduler job id、状态文件名(`_status.json`)与执行报告文件名(`_run_report.json`)。两个任务不能共用同一个 `type`。 +- **正确选择 `IdempotencyType`**: + - 当 `run_action` 同步执行完毕、可安全重入时,使用 `IDEMPOTENT`。 + - 当任务通过 `nohup` 拉起常驻守护进程并返回 PID 时,使用 `NON_IDEMPOTENT`;调度器会跟踪 PID,在其存活期间跳过重复启动,在卸载时通过 `pkill` 杀掉。 +- **`run_action` 必须返回 dict**,推荐字段: + - `status` —— `TaskStatusEnum` 值,会写入状态文件。 + - `pid` —— `NON_IDEMPOTENT` 守护型任务必须返回(可使用 `rock.utils.system.extract_nohup_pid` 从 `nohup ... & echo PID_PREFIX${{!}}PID_SUFFIX` 输出中提取)。 + - 其他诊断字段会落到状态文件的 `extra` 块里。 +- **重写 `from_config(cls, task_config)`** 用于把 `task_config.params` 翻译成 `__init__` 的入参。 +- **使用 `runtime.execute / read_file / write_file`** 与 worker 通信,**不要**在本地直接执行 shell —— 调度器是把任务下发到远端 worker 的 `RemoteSandboxRuntime`。 + +## 6. 可观测性 + +每个任务会在每个 worker 上产生两类产物: + +| 路径 | 写入方 | 内容 | +|------|--------|------| +| `/_status.json` | `BaseTask.save_task_status` | 单 worker 最新状态:`task_name`、`worker_ip`、`pid`、`status`(`pending`/`running`/`success`/`failed`)、`last_run`、`error`,以及任务自定义的 `extra` 字段。 | +| `/_run_report.json` | `BaseTask.run`(由 admin 端在本轮 tick 结束后写入) | 聚合报告:总数 / 成功数 / 失败数、`success_ips` 列表、`failed_details`(`ip` + 错误堆栈)。 | + +调度器内部日志会输出到 ROCK admin 标准日志路径下,logger 名称包括 `name="scheduler"`、`name="task_base"`、`name="image_clean"` 等。当设置了 `ROCK_LOGGING_PATH` 时,被调度器拉起的守护进程会把自身日志写入 `/.log`(例如 `docuum.log`、`container_cleanup.log`、`image_pull.log`)。 + +## 7. 通过 Nacos 动态热更(可选) + +当 admin 服务启用了 Nacos 配置源时,调度器会注册一个 YAML 监听器,并对配置推送做出响应: + +- 仅检查 `scheduler:` 段,其他段被忽略。 +- 新配置段会被计算 hash 并与上一次的 hash 比对 —— 重复推送会被自动跳过。 +- 通过对新旧任务列表 diff,决定哪些任务需要安装、卸载或重新安装(`params` / `interval_seconds` / `enabled` 任意改动都会触发)。 +- 被删除或重新安装的非幂等任务会先做清理:杀掉守护进程 PID 并删除状态文件。 + +由此,任务间隔调整、参数微调、增删任务等都可以在 admin 进程不重启的前提下生效。 + +## 8. 常见问题排查 + +| 现象 | 可能原因 | 检查项 | +|------|----------|--------| +| admin 日志输出 `Scheduler disabled, all tasks removed` | `scheduler.enabled` 为 `false` | 把 YAML 中的 `enabled` 改为 `true`。 | +| `No alive workers found for task ''` | Ray 集群没有存活的 worker | 确认 `ray.nodes()` 返回的 CPU worker 处于 alive 状态;新加 worker 时可调小 `worker_cache_ttl`。 | +| 任务到点触发,但每个 worker 都进入 `failed_details` 且报连接错误 | worker 上 rocklet 未运行,或 `Port.PROXY` 被防火墙阻断 | 在 worker 上访问 rocklet 存活探针 `GET /is_alive`(例如 `curl http://:/is_alive`);若无响应,使用 `rocklet --port ` 重启或在防火墙放行该端口。 | +| 非幂等任务在某个 worker 上始终不再触发 | 状态文件中记录的 PID 仍存活 | 查看 `/_status.json`,如 `status: running` 且 PID 仍在,则 `should_run` 会返回 `False`,属预期行为。 | +| 日志报 `Failed to create task ''` | `task_class` 导入失败 | 确认对应模块在 admin 进程中可被导入(同一个 venv、`PYTHONPATH` 中可见)。 | +| `ImagePullTask` 中 `docker login` 失败 | `registry_password` 未做 base64 编码,或镜像名称解析出的仓库地址有误 | 用 `echo -n '' \| base64` 重新编码;确认镜像名中的仓库 host 正确。 | + +## 相关文档 + +- [Configuration](./configuration.md) —— 环境变量与运行时部署说明 +- [API Documentation](../References/api.md) —— admin HTTP API +- [Python SDK Documentation](../References/Python%20SDK%20References/python_sdk.md) —— SDK 编程式使用 diff --git a/docs/static/img/sandbox_create_latency_distribution.png b/docs/static/img/sandbox_create_latency_distribution.png new file mode 100644 index 0000000000000000000000000000000000000000..2f2307dd3c5bdc91a700985ddd542c16d6dd99c6 GIT binary patch literal 100042 zcmeFZ_dnNR`v&}J(2%Agil%HSD_IpPqcSqH3E5eh6-|nURgt0($^Mv`Nfe@tjD#X2 zdvBiO>VCf8AMQWkc|FVP?(XhRAMf{do!5CD=W!h8b@$wvlZx4iJo^ib?9=Ey1@ETqFMITxO-UHPYpdU56;Uu1n{4y6 zf31Bk?Hrwk#MpeX)0CTFUUsqkhkV1HA5KPYuC7@_{XVV=`)_RAwdcRSDpv80=2GPT z`>V_(m0{C=eRZ*|bKU)4|2*2uuUO!}|8cPW!KRyc{_AU$kv?zZe|@5AVX+bOe|_Sf z3Ne=c&V<--TL`H4nD771rA9deh=m(bBKq<&T>U*mtj-9BL0b{h0HmYK+40)YR?t^uvisNx%O7 z_4nV(+Yu8a{l23#U}Sp4v{aH+%#KfQ)qOUxh9h6X!&x*^4D=maRidP&S}LQfJ%7Ew zY1v*VH9tT9^Y`zSyu7^P5)#qcX(q4FCv2OY90=g>nO-(GHFQ!#LnE;JEs`(^UbPZo^>BS@GndC}d&!$0z<8>>iAMuxDE=cLWAU%!l< z7hV3=Z#o&oE@Gv#INK+vnq{LC(_?IGy!+O=?RzTYuwb-|`>Y~r5f*ZCH(qPRKQdWN zxA-C&Kd-Ye(-ZZ+w6yfqKrPL@a!1u+nJdFTuXoURRTZax7W-xE{d9|X8~AK0q;|$= zX0*#)Khdkau~92aU~Zx>tg&?S;{#`X4pdGUhDOe9R#H-`dFy21i0`xMiqdPOS5MaI zcKGvkcXPg%xA$27h31gf%aeVLXGN>9rA|ffhQb4P9{nqM@xp}*awaAw2FwqYnV6Xm zlm+hO-LZ4$eQBB8w{P$6JnH(}rRHwi>Dc-O?#H!K3olRH9?jLp(u}jGFDGmBMC$6| z4t(2(yIT&Ni&0>V?2V3&9&}AJ-4t`?aKD6OHzf?9Sj9lh6Nrs@hK() z-!nO@_U_%g!s5$w5i3@eFHtfl(=S#zjCHFt=gf@tnAX2mZLan#_20^J&`35b?!cja zDS>QzqJKPi{yb3SnW*u{z@5`0A2~+K4@CV@K5Ft|b@M?Bi=H2RZF-#p1N?f0$8WCf zQneaAbMD+myin$?Z;3jGgA^Z&?b*9G&8g>1WT#=F*Ijc92ze(!HG>=hE)6vQD@_SUIq zxVwtYRMYRw+2@kSuv%v3n(-Ri`BxkRpWX_(f4?+b$keQafr;zd>_lJ5$B(PFZQFJS zFEY@Qy=n7i-}?G`n=e6&o}+(rcJHZ3W)->g;$qd)PoF-?+Su?7wiN^*&i6`5>GSGe zzI;gG9?NwcU$2FUMt^ty3pvBKOh?YHq^B=zth7jMEAWhQ8)|dczbBb_v5M7i>Nc-t zqEV`nwBx{ck(G3GyQQTKM9)ywUS961;L^+Di-?HW$i^1X*O;<*-@cW{??`g-|69_B zGyh5F%AziRHKu1~PA|!L&oXg%4KI5xb2T|+5HWP>?AhB?DwQv2%@$5B`dneVA1u6D z$v1+6f*OWxTe4mn7WrJ68KpK1rkij}?-^Ep-0HEGjL60B5J4j)8)SCv z-Fw3NhgiFpAJ6$WFZA%3r~knn5Cw!eMz=H!5qhld2o=jFAxZ{6bYDd_L* zy;~P|HbyVoj>bGmF|xJ2KiHbrSs$-H-5VEW);aOkZHS#!)P{?}IV~gOL)6tptI*`z z$p!^HY@!Z7WlLq5DGm+}vN(fj*#G&lFEajCwY9Yl!#|IGjy%dNCx2GbZ~XO6&1ztJ%T zf!BK30^0-)?jhCsRbK6_-9J9m&LJ%=O)_^}+yULpOE+Em8n#C?F!ePgok-A3`q|sN zMo&*~dV2Z=Vj}0tNVIAl*2ie#@3%kS5}2{Bbj-}m+Io5=t*tvlc(uw<9n!8Y`9#*? zY#C&&PdJ|tJH2&sa-hZBO8>$IWxF!=PX(Q2U%s5xEGxOa;l%3eBGwHruuH?EqrM>_ zTah@KDz|OgbX_O?;!7l?;gON+ets+Z2L^t1cKWT~DPh!GOQmg#rLARPD0wPkJ&bH0 zgYV@sER@6suJ!cvY-(

G13QYl8v_6boOh+9!htE8e00Q_aOM|Gr~0e{I5&f#T~n4VTdeXEUp&AI zoIdhYj~{O##k#xu7Rt=OVvQ>3Lci=yb}KgAyi+sz0(VQ<(`7H7-q_fbZn5DOBik)} z2U#akQBf|9*Bjqx#QWC7oa`L!s`Lmu?G#lhQ6y&HW>g(<6y+?k2K}GA4Xk!)mz_q`YP7XE2P3N@7@=n*TQM~3yKIXU$ zJ+JP?vYL#)Rc#-&3jA_^BCZnkcX%MXJ=%S&dw6nE;renqBYXP|_V)I|)(z{_Hlw(>c-fgVXON;;3zrr>)3YO7yDGNgb>ywJi+$;8UY=N$WW0U* zcJ0hdAvH5^zPNE*ofmp@K7oG2h7AOpefzeRsuZU1Ow{(E+u(&8H*S2275V7xdn9Yq z1NSrYh_9Mt{iEavm6xv}XU@s#1VZKSw}jifPuwEmnB&qHTJ7aJ_~R~8TlA%GZ}2UC z_wO_A*s&(mjr-R58lOF+JFN;AD}g3NNo-ThZz zN>N#Bo?FeU(_12EihLGWmBIw%$%aSEtr0(Zw4|(zo}HcjwQ3A~*}d(isSK5F0}`

8=zv>IwWk>-w%``g7zs+?D^I-e@RD>b((zoL;kKcTOJ;#sd z2^~X4W0CQZ#LKKANVU1S7)bT{_3M9HvIUW}FQ&?K=r}yUpV4C_+vX_^?l6AAd1-_$6s^JB|d(B z>WJdv;+(zCj9!MNvBpf>t?B72d>3*yMoPH&78S{$p1pEu&Ubf0veTM$3_umAGphth zijJ+NDpkfFaqSN``3~f6GWxsR@_Pn%enEjTVq?{+Rnv2GF`iR{+UXbXtGc+FwiaFG zX!japL^0kMnQd!hbMDotO~Jfc>yI5f#&Y=LI`R|^chR4m5<3Ri^{NC%kdD$kRP48k zmJz@#sHx1hb?bhmqvjP%r5UT%?>ML(i{i|CW)3GM$vH;hzM672@}y15hTS&+>L2ob zQMk`~_rZgWzW|chEX>XKH>4Ow#kgN$J!E#rhP}XZs#^6q5~Ah>ty~{hS67p^e0Pa4 z6ul^$r{#|i1Pm3DGEy4d?Uj-9tU#P=)q+WpAc zjb({(V#nW^G$iVHH1?pxY@MQ`8sdO`046bafA#9s9c1$I+N%@av~+t>+)x{D^R>z> z&BtWl%l+Np*xA>|Tj_*4Lrxj(7aha4`lq7&{3^=NC3SElST969fBxKLsI5SvQpob_ zuE&!T-!rW%tKDv|-+73810@=zfJU=vNY*} zv)g${Nk{6+ux#2oL4*8_BA4pFdKO{cFlM08D1kQUW?N?}is2qkP6eSa{fM!9CPzf=esHlF#42x(8I{Cw;nGTuP;H4e z{TN8=b-Em7RBh`_Qs<0fd&;?&$7zxVgBR&g`)eu zy}kXXBh${E{JE|Jm+C!G6m$Qj-HA8CRpeA6 z+~j~dUmc2YMr{b6j@n4^J)yC$Cws4J$;w_E9v=^8%uY*F^?De(1yv}sV^qxUhc=%M zP3}U0$N5*Wu}_2J;)@0TwhI=`M~6q%mKaedV#c3IxV$lWt`y2AbZHhfn|HmINyWI% zRRqiV&rW~~oCWhMcae0({P5{udy(MUZG5l)MPr1tx2|Gcn(0X$A5@vN>-@=jc00us zH%NutTJ~Kq4e638Ott;X`}fD^7pB_%yAFyp(Ou&@|0ej4$Rj*Sap-wU&u%*HL3&9P>c*q4{SUgHaQNw~sj0~!pDRDt!l`63 zl;lZTVLB8%1)3PoL(L^03t?448(uTMX&IrYr7rXnfWiN000eD$4ou6p{hW~}FnJofc;-+SZ=)M_q)1T_!S>hpjq9e725_r>wLvz#Gk z4>xrovC>|wMp-Le*dr>c+0@kZDNx;;L*tKWwNcony$a!8%2iJepA0<=j=d#|G9Xz{q+f28>*g(okYitlg-#|YixWS z<*KBvZi}6ror8;uJR02R(%$~SM!&i$!*qL#=0}yr|9)dY@}wWr6gzqWttYB#Kj>|5lP7%Xhq)goefdEiLAJ5*DeZ(uMYq-W>url{^9_nrl!gQ z4gVS%3PP3r_4VZm+QLBk7wHo?d#V|hs!3M2LCYvvTU!U<6qi(1GVI>Hn|3i~%jV7a z-c>&ez1b!wCu(@@U|xbfGHI40kwZ6_`rLtpkF5ol(UhA z<+b<_`KJIu1dcs>#@658zmb{wHk+8;r`)x9PCe_l@$23||4X3&M^L-p1AFuJH6)N` zLOu3gLn=z1($%4s>Ww-at$I3m>W5OGpQ>EETy2o=CXxh}ty02zSGytG+x=r>(22*vD%v#-pF> zd~BqnG&47xP;}r0)+4nhWdNs!RGEUTp%1Hxz_JkD?MJ@5y7e@kAw8rWv0z@}<>mFt zbQ&?a8a)_V%#A27N^DF_D^H$0xo_XTpI>B_g70hnee|xSMbG|8eUAnjNMYOWOjw%| zfGcO$^AC1vC11D>h^(7oxeO=tLY}KigNwX^0zLf(_In61EB(69++qz;k5UR}*W%A1 zjHt}wPlc_PBSuf$USFPDhZUh9T5o{AVU4&wyZ9PCA2>+5+Pb6PJ=kK(u_#4YXRjX3a5_)UA0d$Ix4WTq0cT zpFfsJ%Jd+IgmM$hejEsQVjE~mJK>tYjq9B3UjxFnp%AUcKee=cZK8EBK0baLINq!T zz3(k-4C$oMLat_HEXA5R{gD3_uO7#AyCZ3Ftri*89Gs zq=u@*jGtQev%u8A@M9b2L?&v-W=1wqIayf>IxuHD8I`}CFU-mxk(U12XFFq&Mtfv> z^9fa~5MX@_{_0LZKnWmi=HJUgLPCG)W*%}<|BB?%)z|7l2i*0Asas4o@k?A%)` z&pQA@@4=%-(ISax08`V_gmUh^wQ8oBYiC$Kw8;UqC)p5Da3RNG8S27URvF{>E9(<= zw%~<=Z!vB^B-i(R>!*B$4EuHiYi$pY+{6S!B$IJ4tJY0m6t?E=NkBVa5hfS}s&B_p zSLMDYWkto~I1Y|;leRgzxvbhAN2u1>w{Nckzj6mhX*cLSJWMI#)2wGD;d-9r-_L{` z67BXRIaD94I4{V!-RR0WL7yZ{Pk110Wayqar*!JnxzA4hO=${YO7TZ*s{|`j42zP* zH+bt0u_*QSB`3-pJ$kf2KmK>H+T+1lpPLNK+nl;TZvs~HpV_}}-yQ66pY0wQ0f94V zdON8ppPaoNTjSLaJPZ!rM@8clX_5;<(7J1?E6lK>7U!{|&==VAMRl@WFc&pFD{I@T z4ea%b*`S+B1w+$ax$Dt=>ae0$ZPt2>9#w5V*J<#_TmP;dqBh}tw%K;AW`-pa;^KGk z+1%a6_kKv)IeUkEFYt70cHe`CSp9G&orOR_j^wK-Ou|G?A`|? z(TOaLn0ga=EC5p-PvnMBLBnJ>eT^jD^P*KBIj-`}bvRc&6`?JB`10k;@6czX3{IK- zm$B@mn@w_3(FE$efFcu273oT_7oiF`p%>l{W;nA}zsN^F=k43_$&tjK zb^1wb-sa^6AQ7MHeJfrk~t5x>4#P~G-x)Q537}+>_Xy?aala#*m1+8alKxajZluwEwAZc zxBVM;WugtMEX&KuIW>1q6PY-asuizc(*5}fty2;#<_>WO*|cvD`;kgK*{Z^YPJ}AN zcS-HOamU|(Gf+M=FnJv-+I)e^6X`eCssKcHKwU5a{X(zzrN;b6j-!7cKR6`4nfUXG z+Nz-z;2_LfnGkqq9T(X|ZN4f_B^!Kf)2tMr$U(m`cZmSq7te4)zvu9-^!Q1r6#IIoQ||w8JVrlD8e7k$4SaSNDvvipu$Zf!9b9w0X32oJtm^rh5?@ z0(!~#xHPLe&a^8dCP}|6sv{nDZDL~D_bg0JY6eA<=3I8@WTn9A)2H88Rw{*lxwrkW z9FBCb0ABrb#1ZvSWFZgT88FBG;F&C`Y1!FflWQ_jzRE`{zOQXZK$=h7Urx974yZdz zs^?hu{f>#f*52ByJ9S;lb0eXRadvceZf$HwU;P0s2&3zMWpwdN`u~wTBc18;{>(Lh z|L%WQW4&S8^xPzPI4f5-aR*FF7*faaWhg~nn z4>pyRDNNR$T)i7KB#+Xw%)5@%RfD4SO?(vf$8gZ~glrMzug|^oEgpC#vigI@TlXJaQsd)eV|6p5%B#DWU*|qe@tOD= z!ndout+Il~$FE4Kb*G#`^E1>UekyE~2*c zD_tG`qZ&EWW6{0VRkiZdr;XTsCKSJo>aoh3I?L{FiSn6^&xi~$kT7F(-m=HDSU|By zL|;&sbb(4oZNCdA3FjJY=Arw-)7 zXqdz-h9e$aWsSh)tUAC;bum^>RYK!$R;Qz<#};z6f$}J^u>=fWx$l#E#??lQ3=9nD z^;HV$x`dnLwY2RF&6S42+&@RC1u2rr5RS z(wvVlO5eYCPa|LJ2uR*wPBiPN3l+7A`gxk6hR2WlCgyFVnEd|qkTZ<`=+v6-HU;4I ze>g=c>c)+g%>kXDcr5Z5ww?e6Nz6IhweQ6+C@+6^yYCsNg}i|?&8*O8W!B~=G^S;5 z@28>PDKM{bk*>@YNdH?CbKB-AcoeSK)&kFLK3kw=^WNZ!m-P!j@v-K;!g<@b>^b~S zo4W{F?6INC+~pny8dx`wH?`B)gin80fCv|Mmwx(rQ$Nk(3%d?G1e@#C?pMetUOa3%0WoY*r&VW?6dmY^|8wQH3i9dM6hZMMCk&g1%6 zkCu;+!@Z3xEaIH<)#?TjdcwI%ROEWJKbxQ?KfG1oHM1FUUJR;0SvMK4`k|~W8mLj> zY>b@M6NCIK#u{R#mp%q@ob;n-RDL=HB39_?qSxct^1yfRWbd&{Ni_Dv1Z`Z-u#N9s ze{=dKc6PhkjI!rn#@8?~JdUiRyf7*CSLkt^pLUF?ofv4*5>@V%OmII#7o0a350EB3 z{d2`$rSNjVJRr*wBrYl%9OI@;Yi1x22HCd^F_5302y-~y4G4J5Sm)Aq_?3LXHu0;A z`QuAS?xfKo941vU442z??-TssmQ+uF;4Z2x2PyV%?(Qf&*1zn#W?iITH z`&7)RjAO^j8yo3|QmWo2+1S{ud(IYu0;IqzvwNk_Ocygi!79oyI+Rbj8lEk!oII-E z74Sv}!M{Ub}>xUxxk7tWDHOr$<9cc`##RUKz@-+v)A?T?2mF&wLK_`7lID zmh;KbIy=#J`S<91qnmm8jZsHVZsqdj%g=%M3l`94-LDud`{a=MIRF!lHyWokWjKLF zYd`$+(Hu$u0R9O@lr%GMG?RGPhTz`-P=-ig&4^t6P<7_OzI|MUskL5f^ub}5T7i_c z26+Hx;`Xao`~C?Vgk4ZmS7(%d^?5zOe~|R-Y7X!5O#ohw$o3HuE^FZiC?(xypO!7c zgk}_m*D?O|V7CI@XESq~2ak%1iaH#qVJuKdYTJ>+b(a!mrl+~OwZI(2RKx^u&s;P! z+r`OA#0L;EH*NAmLPDy;SO~Fe48rM7V4&u@*C04|bXP|PoRyxDOS4h*e)sGzns~*Y zRVs>o^%}4uge{&qbxIb&xYaN2p0T3rbAfIHPScyWZ$C5V5PAhqhdXt&(6JHI_xoSNf^Ao*)-=~H3+@iP*mQ7r^%=lv;0cI z`KV~fzsw^ntRbr0=>Fw_VBz#~pyO{6QQ-SJ(8A_i?(*0Cjss|lHDD35i|D9ybP{qt zBeF_8`C<2J_DUtikF?~jRlzBJiXU|~n+s$r0wHgFy5#~4IQd>gTwHrwKka`tXFb-~ z3@@VT>CnsqE8<}sxe;L#1H!Wtxbg#{FQZn{WBion*|hX@^sYM(LRQdu(2O9whra1j z!T8xzr{1BzHWyqZEhCZKAqz=VCIuCGW<$a-<2_5A&;{QaDflcsZ>WeD_2(sUS@Uzz z?4dC5uOp*fN~m>zRpOkJ)xK-$^gUK=ser`Vb6aSZC^6V3wCay1l*CUI zkfd=iM6_K6`48BpZb9@R_N2faNA@s+B;QCFC1^b+hF&RrN+;umyFd z6fM3dEEUn!a@@x_?b`C=LmD7$lFSxn=(9#lEPF>-7&zQEnRNmC5%G`FeJT94gg3tNR%TWa~aJp%+6+=K8`M^p`v)`?mv|Ub^g~Q!vn3_Lcc#tk=Nn zfbM`;*chZ+t_xfnnah<%$kt|Ie{5FeQf92#n0Yi#IZn4o$A$8oUu*k4qlPuhx=}#V zXPyNiCl55&kdr_Aeil{&Fa!@78*OIFnIOcAdrtCQtd6(~et;!nintoU0DLMG7mMr9 z>Ua?5OlL2rpitQLh~;E^=Fy{#EWPVa=N2xG0}D6D=85kNNaLn*hF= zHyqmjDAHRZuElb8yjQJHW7fA0)~i&X1rJgw8~kgCcL4nb@c`)-A5%n`y(8^C`x8Mb ztS!19`ika`Lh@o=aY}amb3+v_dU=wMDbsUQ*#CxujPL}Kp; zs|I%c7~0B-tRI`VZoNqyYEabVj~_pN#$OSrWr#cLpR2GrS}p$Pz`%XS&a#(8Cb@k8 zH52XPEt8F>pbMEBib`XxajK33a!pjbq2TzJ`1$%~EE3@?@#wRoZi7ZGS+;+^zVvHz zH~WZZH8wT%!{NJNowIRyl-KAQFe~@$&Pc?8XTJt;Rr3A2?ud03kV9{AFYuXp>v0`j zT@%P|a0%SX&K9$=wOvMe%&GDo#l)##{^^e&Ka8NDcn;71so4uwLp^j6+kNC|J7SVB zxG|%dsi_YN1=MG0qF^OJ7Wo?{UGjPO`l6Lp{8;f47Ri62uaP$pi*y3?6S14^DC+lm z_VND9+X;%rx&#^aDw$Uep70vNc+DJ|9BfVaX8swgK>EbU)|Rf|BL>>rN3lMoKiR3)W(~|Sih?v6 z{fT%fujzxHbi#Y7opQb3lk6jEXLhBS4fw0tg4RbYur4-RMEwHwCsFhYSuAHf^HD6aOEJocbrYU1&8&R$D=O8Bkwesm1rFE5rr%kd zyH}!a_?F#g>r)BKoJU*BB62C%<_DPS2ce;)ps_|qJzAKXf)B3swNJZ?lx2h0!#DNb z8c5enMF-FStvi~Ho-hWy9W7MJwZG}*bnQ6Yja4=CkVbB}4t(DpVh{S**gVAjsD6^J z2r{>ILDR}>=xlg>jWpL6 zfHT$vHWUSwsL)VOTMk)wL&N+lLTKfaMfj|DJ(pHA%=l~l_89lM{nFCx;5bG9UccI4 zen=C|aaHZ5^1L%vY!LF<(S+8s5}z5U>#En1D*r@oc$nVfpS&VfkvNLbMy#toyZT{Y z%Nv9Tjkz7V^M}#^=1kqwhczLjol7&3PjW%qUQ}UFIElldCzy5l60#a?m-Mdm^Q=yE zBOuCR5SYYdAl#qb?o$FiC9^hC(p`^SJc)rVlD93aW?r znpH>12zrjhD%mB=P_4hB1a9Ks*nsvg>DS1}h&W_$6ZRorFgXCZoF8W(rTrZ5t*gE4 zJzTsB4DQ3$FVJ);klVhD%&#jSur)){gz?VO<=7a|1~>!dKDxx0bM!JYGCERJ&}h0Y zPtj5lrW~wx{WC31I1~^DOtEJo--yfu-moBG7U###7VvXYp6K6_hW@yAQd9Q;z+3H% zs&B27t=-UEcWz);nR~Bof_`2WUlSEzfM#dyQa)HfO$*jNr*!&sF@)MjNgj^G*#oGU zH2V%&#=P$ZWR%xhDbLGidg!U0yG2B{LMTr!?%eXV;68+(uL)X$Ny&;S7vB2*>soUo zlI=nWA!036|MBC2mWibP%gA#D9upZ^&4u1`U!&z2lAL4f>wsYX6#2EX5Ld2_*+4t{ zCz}qv`28C>I5?UacI_Jbr3_A%$xz#_C5sWk7?wF~j0$OAw4fO(eemG9hR;F|n&X@S zbBIl*l-VERfOe6O)TPE`+k@|9?@DOP_VVU`k)B&eLhZ;|mBtjqlQL{idfh6_AOE&Kb%o7^U_uv7OF@;w0rGbXRE;O zOz0cUl@AcFTv=SNwlfj20q42AmUvKeJg1TyQqi`RKMkdSF`c;tYK+*7%L@U!EV*oCh!N_&l#AJFX#o}5VJ zKTDD;n9Reu_Y?(Bk9+G}z~kst9$T|!4N)S{ZNC&(fg^VP4)JS5jIDoDb@ic8i#Sdl z1mTC7mehdw7M63;p>yNCeqhYFVwsQsMEVOCwPjTd<%_?#gKU*UV^#zyji5PuIS zMTLjo*{>A(r-0r}YH;tKJ!ZvM?eiF7?%Y{@4`&t`_PnG$G~WX)%QhXp`1u{`w_{%e zf20)*$9vd9BO($fijq@$ooZ{(*ulIW<*+d8l4K0l1r{LjT}sbH95UqYZl($Me2Myq zsG}}*f`(ji%cZ<(q7kGC=puv%*5D1Q`2kC_SXkhwcJGc%$ z!@>)^e^k|Vb)R;K$XH94VKN}XB!JPnA@LaK6^EYcou!0Tzl9jw22d{!Z9zC6DhEKW!E1j=3mH=uQ>thhJv7UUg# zE^M_K!Ly$?X5(`Fu=m6?arg^zOQo+I&A_QWL{p#gf_U?T&K*B-LI`E$Luo1Z8Q9FM z=K8!s`XZaRpqymN>0A7ps3_QkOka4u_4nt#R1-hx-YLf7rdVT<+E&!qyehL;#+ z04~sqf5kTvLIRYuxZ7Yv^{PzJNgQ!{;x@TVocm>D+vxoiGZ2m z4)}my@E;Q$mqLu6v5Cvep+_0R*$caUg|8Dh;cHM-yx_EIuFyk?v7YV-5FAGFeL?0g zF1$UomRVpOfE^q_On?QN>sJ41&C70BQWX4xRz5(Dsz}uBjmLi?7$Ff>sk%FLbOyun93-I;DH-~|zu-Nnuz^7`i`t@I=mltkr@?EI&B7m65ciYEpDALi z^;wwp*B9E(n-cgeF+V^5;Kk1@I`4kqIZ4}HpQNXCX;4N&f;HQ|op=_hnqmMgaU%Ng zp_vJq;avl z5L*Kc{{ob@v~OY%)nNfqvL@7@IsXf4TwDye#6L}Gd%^0ZfnR>?d+4&$g1fuBF?K!t zuti|QFhFDn+9S)_S8J|)p3|hhGo^+yjwv=YoTa=#jiA>nQNI*v*_!J@-0ZK6>R~tU z1W5aq%e?9M&j7)~cZgYX)L>5|ToGuscZgOMy8=08BsF4fCXz7f3JUT$7z^O3JvFC# z<%;-v4rxBmWf2oO(jMbn$S-iMPEDNzX-4J*^jo03$FtOji*T480eFLEZ~67bGVsco zWjJpPd2T}rAm<6`h@?tL1t`q#fbg9Q&7L1|`ls%S^aGp1X2nGO1|YSu3u}R@SEp=|JKd%3tySzlE;b4CHgst|@QAmy+@IY+K~hkP_V-m4DwSl7Y_wMh;Un+AVh z=?GvlY_=)QVJ8OAWh%5r!CcOSH}Q>(+zDRj7T~@0eZ#kxiI+48(T(W_Hu#V4U?K(O zf8*xOdj`UuCY70=e|y=S%!7?+YxK1j6;(HAL9V+6u9jHLY7g#S32*An?T0Utu@o-M zzo6aSxPALw&=bFqGitLE^|F(TCtm!=K7GQ9B5xxjq>fd`Za7TRjA`osgihzF|%Pbmaf0$dVe;$G{ z$lO--&cIn(JCdE-U?&Mzb-&nJ*zP^0pB0J`vc^BQ;q6=ld&?H< z`=;Qn#L!=}kq7WGG2S#gP@TRcXr&1bAiOd^iVv`w6`_U|7&Alse!`JB$ZW=_S%BPV3s+}wan zcRy=?am;VsnP(IEmnvK|AE(XLg2QI2H2y473#%bI`V)?eWqb)9`rGro-o9NVZa#Dw zDs2rQV75SCrp;p^`IHizIm{KeGv0*`4LXySPtdwwodPmi+s9A|?=_#KS#ghvErXJ=mlo6VQv zHY0IyaO@HH$G4pNJ^H3_?=oP8()){!v$*XP&iss~^zrE~Rw<8+HgjoWi?BjpZ&UAa zSiv>wJlB9d_|qy)>08ws(BXbNlB7u8j2!yysJE9_{`6134Yc#3RMU6iV{dC6P5z+S zIRItQ{DUT-VDu8Jt0@WFi*-4UroxtAWku&vHvUP9B~p@-4sgDBO|`l?xVedWNP}@9 z{u*FuP0gmO3lm``QGNE6fD%GZ-O2?EPvNR% zhIId_#*6p?aoYQK*u&_D>`_tpSsmwE2J;2nnH|QbP+lDt=iN2p)#R;n)Z8R^Le>Hv z2NxcsKlE$_M95psTo}<~3PtM=2nMmKAY%}R7a5^zE=&l@%R7pmiYk(b+B`j&H^h|3 zqHf)me*`DOdfe-8V~U_Pe>n>wx6nWNL7!)lx(~XT_>xES7nS^(xP2j6{OSL`@>4&} z2pkOtSstjm!+`P=wA0tTb}lQF4+6lDgjl8SD^ic6sruR9Z&RC3D|!EZIXH@sfDq>T z$IKcJM0rIwg#;%hB_)OgWmi>HtVLogL;OI>`Lv@aZbLrMaGVEFBWWYDuBEy87f9%bnOrZ=qWuNoNYE!C`m0kqM09j? zjBzBI-Z~vXEp(zP$C3`*j6W2X0oHH1{@|YLCH!L(`AZ3R4t`Ut*QfXb0$o`uIVmym zAXIQnP05^`K@d^!I$~C=`+=I?LGB_D2?PVHls={`nDm66!9Lz!s2&B2C>exAanGq4 zt6uXzm1ZlyE)$mU@Y}i95Ex5squ3OJ*zd!&tCd!n)Q>uRx4L0pJ5}JG@WN=NpmvVK z%hOZtNMe>vsW01vr98&30l0PK;7}5`7k!W8?@ybEy_(Zy=(U}`=uu^Ch3H1WikLEFU}Iy0 zfd@3u21Zt4S-3|g==c7qMO!`I-@FC=(L2ZjjvXJ?Kd7-E$LF?P;oTN_hyaxzB5R+Gd>jyt+J`3o0z zNJ>h6#Y7I#5rG`*g4D1MSERm&hlRBGawktJl|cRu1ir=dvLDfAb9eh;Nu$x;gMlN0W}D!dTzldq&(%XAHHKNK+EhkH*x&1c6--$Q8g>1M2s zI~!GoSi1Lj@C}-toGT-H*0W3AL1*)ebUq9WC$P|<#>$$xem~OiE#+z z02unqPO20$fuPJx5i*tqTPMUV@>b;J62iPez&uny-=7E~(<7ba@zBAJ0J#CUVDxka zq%ty&hTW}+RbgAXYE|F5CO~i&0mH(3M7$t22o&96P=b-I0l)Xd07FnFA!6Yl0$TIM zTD`>kST@u41-bP<0M0$%hUdItR@)1JpK{o$FZ3GtVWhoS$6w zT)>3`WHgXP%0n0~tU$~*{{$0{LF8o*+}nxREPSbcHRAXhoJBg_)&e>~CwCp5u4@`I zf`Fcik<27x?C^;zS8_Cs>=B?4R2ZRhuYz^0n1IyZ3i$lgwFdwoU35(>P5Jl3R zp=Ef1ea14w0^s}xz~`EX>9BS7PbaiDxLl)Tjh?% zG$Lrx0W~EW7O|su(+1U!6~2p!-o5A?z}YxBJKsP{ifZl$rDPWu*C!kOAl3x*KRH*I zq_b?AeNm%2uyC~nDe!_LHT+w9>xR#-$36f*A2fPLNis&59PSe`x7(|5PZmIR7)Zky zQYHyn$nqlyk|ZbPZ*R_%H71k%aL0R$IRk5>lTQO)ZO%b3v7HxKwQ3cgLH@sb42%9< z%ntoRs=%fnQ&ZcFKJga}9fU$a(?ka12(5Uz;Pe0*+~a`C4v_ze`-ODq@W6qz3<5>V z#C48>@h+m)lY|JxE1jf6If?iWdDAQZ=N^#J5a9OphTan)4XNnj($8ZbP&x21WFB0% zq;PT8+G}ng+g*eF#hwa=|GjG@ddJ;+_vG+$FvtinGOS+x4tYF%p-T`*RaFyQmStn| zPITh?8PhQ#4iDeYo}Mr_I#QXUvjhK8t@zGO56ej)zKa-q-op+cWw}R@SXYBMA?LfEo zZ#Brb74`yJVN^F>yc#xbxYCaq8U54~*h-$DOC%s)fI|Kkz6xXr_Ceow=qp*G%P>G} zg(s$1mfVLDy~Opum!+r%V$x*S!GjxdfQylV(CIek3v4DIaT_y`@ZsD7+3J6iLs?bx;W8(b=Omp$4xacS->??HSVhd1bd(V#6t*|12J zBt|>5LFdqBGx2F}1jk7(-XLRG(AZ+aDp~LizPJJkA9p4~+!!DC<(%+U@Fj#tgv;~b zm0=_F3kvqq5Uw$;&j@ynT(^%nT0Q)|I58r*`>(PR4ca`=O7y=XopY5LUj*O2K zcU%rC!58%*0ue*t zTIh07y?dvP!}rdaNHuido zivDOmd}~^k;P_<1^;GWq{X49n$X5?UG1t$T8t6Z*AA^K=`T6H1@0hltdozUggdiCJ z@}RHkuvnI=U1T2_o#>jwHAu}@&>Jk1h0pqn&DiJlC%Wp#lOqpb=D{678p#)QC>w#? z)Ray8rPs+43Ll=u!Y=B;Ahc2}1rcC>Hj@#Xu7@o>=S+j1K)_dpu)0hZHg{323P7Ks z^$ow4HuMcAmK510mMppHW6rs68tV2o%rRB>nLKF)!9quYgvAf7wexF;Orj|APrc0q=3p{;);LLr=X6beD|(EPd` z%kQ>%gXks$*giEiHDpW=^67AuSo;fbPq--QBgl9-&*(6=vnwS)d1({rH2~~zw}rt@y2>b2Na+KV)7(p%AwWI?dmX z+%D-E8Ns??PsAOgO#-9iHjj*jJ; z?1TY*V|erC&0mo6WRZ22QI_V%Hjv)ZX%-u0tr9hnjkFM1$GLCc3-Eoo2W?s6e+Voh z&Xegl93jN>b&U3rdyQahscslGry5v*jJbVNe$qf{;Ky6_gu$Jxto~rMOF(Z>4#Gz@ z4`Exkj(EgyB0j+1L!l7+652@2g5C%HNX!MPxZ(&b08v%_%s{?l2mTT56oHtsWpCN4ndU6QUq-QaO zEdhHaudKWd>d9@=lwsBDp%}Pd5fc+5j~f?WIpd^#WsSnYRu6oF149h|%Vgnn-ltV@ zLQc*-$mI%7bYGc-*B}WJZV+dXq*ha+L6nejc*;hwDOG*ZPxO&#S5nd(ejR@tTPMkZ zd~rdj3XRqc{3OaDy+HTH`L_*MoOTT1Fojl(5dTpb2ItPBaS0!2mP5Wngl(dHC|X*y zuFb$9ch)aC6D7?rb@Z~6Q{5#U7fAHA<0wVvBYY5)YEQw0@y4n~HLjDYJQOxxSdDGE zi3W+X3(5Sz=FOYUtgzSuneQiYtz3AN=2mV%ra?Od`Km8h`T9C=D=KGDNLFz?~w;yUTh9gpiBL50#&$aQp> z<$L9b)I|S4hfEH~c8srBwLVyvZvk}RXM@82PeXk+AVV-?vw}j_1GSqbOeW~TPf#et z0jSb|!Ye3JO%N}*edAn$CO=_E043<- zWI)>jAg#e360im?>9rblEkPz^x~lf}jT`T9PZY!w43!;&rf5DEH!k5 zy}sZ43Gcn@yh$VOTHP4=NyZi7P@uPvl!cBhUf=+VPl}7w#Oys@JF-(|1gy5wB zB`McX=8PcbV+>&*K}aZ?;EF&Q?Ns1jLhKN`93-gs5TFQdL!3#^51#ZP^OKc3DO4t?yyZ#QJ~AD(GGjm)J5u{)x1gIQ%%|LN6>ci0|bTf`_|C zaIcbIWf5!%xPf3jbwZ_e`)6Azik085UbyN6AJZm-jm(Jd`7t^@x8_QW7jI}HkxyA*_q z`X_@>Il|#>!u>>hB2SPhpLC0}y%CArAbxQx2ey(}c{?1JQI4-B83HQ~#x4iey1=m0 zr}5gaia4Y^%tprn1Bj|dB9o{kI6q`y9t?DrUF#e1`yNH-WW*uqQ*C6W6 zd80ao`PJZ})b;i(eZ$|iZ(HnSH`>{@152}P*VvC1%wY8zTv$!$?21~HrtSFgFb{Y% z3O#t6|2KC6tfN`y;X{W~ZJG~}+d{y6g2j}(xppg=Gy#8oYT`$t%S=DHyp8h0W8&|{ z_`^KqRtt(z$cc*jWrWODj$;plRTtWm*vT;sw~uwXF4j1H-CTnD&PnNj8p|k%=@qb* zMAjvI2?5VWoUWIt_hOM(lp|MBl6tIjM^7m#9-4V_1{nocjQvEy2o7c{u)@Wj{E;hY z^gV!O zL_tD^yM;R=2oDFo(-=j_V<_^oyCKhwcc>?ucI;ROyh_+jY`7eX1ldH`yHqD51P^@S z`9_mgzmE$MFiNco#{_9#a5b=mtFpUh!X(IV*vvP9wDg0c9tM9^u<%$HLPASadZ9y1 zvQ|cl4&~GFMJH5JT1p44LzXNtR-62gV;4* zWsu$lYACP`Bl{{C>K|9kkXtufQw^Kr6NW$^*<;`#|JW!d#V83OESOSu2`~|$mW?;XDx6;8s3OuzciU24$ zRTAW!TKM&IHNe+j;w!GkFYtk!#)#i5G!P?$g&qnkF`__+~?DRN;AO^+Tfyqb{naK5ao0W9#3e?Xq|)cU5iwBXdNrM9)xvKVKk+Omfcu!Pt8M zRh4bs!pF7^ts>f{76YP3Q4vsrNH(H?fPzR8Fpv>ZqGZepMxua#5=D{{C2OM~lCwxs zNs^T)`HgwJ_xtPp^Nny=Nv z`z);rfhu!VXJDbXvc;>v3Sn%3aRmYGW{r^lM7FY|ZYBK8#_%ktcGkQ`KTCOnzIZd8FkqP=I-OQ`%+vlozj7&6hM=SduN?3iG*;!*DCOpq;}rvXq+sh-YXn zO5|5Zx_X-!D$ZhRibG@ zuU3F7sk9ddWeEWP8o$HHx|atxN0EV=Tlmb*oFF6PUtmat+BV+j5SC*sL3wZ^oW86vtt7lO zH`eVPw3gn3o_swzI-0Qo!QAqR#GdMCU#ruS-&g!Zqi&vkRMp{@ZY3gXcuXASc_3r(GKt*!g&z$iHg!}%Wl+n_0OzoVx;al-4h z^He&x?olUXA@t-qd~Fgn#zzkyZYXxZ)=3y9SR)(|lkav*gqozHL1G0PA@B?Gkvr1j zJD`LuWFRa$P6oImA^r7h5|Ft=2N{WtI|2|Sr(D!}l@*qZjfirZPa9FgphvXPI|HP& zvj7A)N=TeS$(`hw-4Q?@qFO(KjQ#$7@HiDhm=vJccnrK@!5g#TSCANI1|d96Bny0m0Y;MYAmcPi`#9o0oz3>8}lgXQoh9qz!_^c?Ghc ztP@<;Nc0d=l=~gF+5QlShTt-5SLfR5+`4xBz-tFN-UyxZzD$kUnj$XrIi^_2)mv_abbBoSh(*}2O zb9mu+nOgG@^0kU#*c_ds!BW~l6b_@{OO??(*t*>kBdAFq#5RIN?^4D=z>#_rQE|7X z0Z*)89K^~h=x_sNxo{3PbqD)@ZR*Sou}slx+CsCp3U!dy^pV`e^`rae8#h<0>ME-Q zcYF0U&s+4*hT>2p*kn3cyXWRjHy~*X=gpf(a=hCV6t8MSM~iQcD;VXut)2JSY{meS zgj;D`TUXV=4Uz(s$~Sl;qf|f$DMxX)`Nb;+Wg&6widCc;((74g-CneM)?PF>xnno@ zOJu#evIay}p9)}~`#Itk?*-B8Nz<57+#vD7@}eJDL++U5Ry##+CMG5lVTcr$npS;^ zxXfAOXYeB^?#n{1>*`P)@`^*=l*jvJ#`n=9E=l%ZXkNc3R|~%rdkJ~`ioFKMI>Tf= z-%%22`VFuC9UV59@OPj|9Nn&kKGva_eEbPB_C^jZJ8Dn*qj=D7h=w+biD{B-XSakp z;@ zMOz-{nYKGMrY!k4$uOCyZ3Aoz7W@r&?iu$M1z(@OGqY|}gUw04VoL{T0TPT0FlZU> z_HQtczQN;?gqpHHRRi1#<5vIZhBbAb9y0`gVI#D&4yvm^hM;o*8j>CZR0^!I>T+1& zlz$BIhShn~x#53j^+R`ZXvPczr}mG+rb(;_0j4`3NI4>s8Qi%(2}%1u8RcLSzW?d3 z`y8z7Q@iXPQ3O!-r?bCXXDZnZcyx_Ij3yLXDB$(F^=zI#;~mT*pS&mD0n&f2a8XFN z)cgX5aikj*wg|UE3$UNIxg{gKP56mUnT=>6N>3v`=KlG_$uiD{>Vk)Y;i6*5O0h4x z8=lsS294;!wtCbx!sTAX@jHV-66W-t`WS1))Bnkc?=%VjYUV4#&HizbEMhPSF2LOj z=)0Ze7|tBd`o3cSZP+i^hOcpPtJ=r}jnq?HO`a*ADNf6ZoEMqU=&Z@jh(RvNeGdR* z>9CKeXM*{9nE08!vePs+UIv@C{wV_3bbAZueI4%(lGQzRH*-(mCX>+j$Q`#O|iG z`8Re|>E>x_XsmWT5+3BM>`F@5M5#Fle)yjZ$Vo>I%9jBJS-=QEIpiCBSi(=}nN_Uf zLU{l4N*7#(er?RBPwLxFe+09a=W*fkygxJE<>$)}{;`3Y350c;hq`>sp%nWm)jasT zBjTs;b%~K)`D9{^?>06IM{%Y{pV;0xeFg38oH4nk=hih@Q8^`kM5^{{@#-FBlzV3F zMTg1$?%dme8;;ESC1+g=phQ8_CVc$ilKp^NmE=5==O`9iQ#SXWzmI+Go~PCdA7o#; zr}uIE^Uveh&U;wD$zQLA%CBSN%x7n>h}8>$jhW_4k$-!Ccj{8z|r)H zZCmkeMzg9c^+DIUqhIo{(qUgb;jc}!L#t|!KuG5&qS3&?^7D-cI`;@g=$C_q1?|M- z^SqG}qh|u0Kr60z4e!=nbU^TQ`!#l{oo4&q&uGs;E7_{1S6Iun?&X$I_cG`rEvVU zeh*%int;@C9jF{*12Xw%g3`mgk=T2#Tjk$jgO-^0H60M^3qQFiD>GBC1st{QjfZ`= zsmMgn35iLFt4Gu%m)`?g-110`44c>!d1$xjEDHGUlnd<)HnImHhThJ|!_PuiNsNC> z9t`s>hEamm?Cp*+&=~$P*WEQ)M)Rja0Kf(Fjhjy z_=32T$T9-j)<4{;T2_q?ovm%Yf$>du8v%xHb?@(ius?lP@%W9K?Fn7=qgCu4(4N}9CUV*%8Lc2&MCzv@sVlGV08e>-5(}|N zYOOF+{4GgqRoEY2kWij{Pl)pgBqRM#l=RHaAf+R7O_i(KLz2wnE>5PPKRfIl=9`nj zDl7TvCVZbrKiXLQ2o4x!jam*va@P=oeDu-e_On~3x>}yYNQ?0_lg2Q&0lpV^@~Ia*WrwGZU&W#5nbQK*gYCR7u-_#DHr+MF-^a70x_owG28#Q6 zg?Ro?n-LS!*aXemIB2tf7@&r;U+n?r@6e)l2)}hiQE??f%?Q^twGAPrqnMI?V+o01 zD{~xZue6e8b3;iL7z>*A6`d?rD6n&9XwH<{*bm(Oc&>K76Up`EVXF~ zM7763QG9<1rC80p^pJn)`fr~(ao|L*q;nf^;}>cle=x3Iz4`-5HX6L-eyZVHuQM`cGr&}je(WncB7C4oqp88m%j-H!X$ay` zwB7%M2DIdwTlSpk=+{Zj@^5i8 z4K;_yq|ko_7+>wEXJgC1>bNvRptQREAwC^>9ZBt?@ZfzSckX;T;QMRqWFVBBcM8;L>dF1W4Hp}0#`uAv;i%+vls-~f$<7^(AQA+ zT)!OY(JZ7Oe=?$qM0XJvbLum01WT5Stb)OS5y}H#v?JgGq8q9YW6%`?&D?}wL%s$c zk{T(0;_aYF0aXc@hECpV&pLP_BwUVLAXi6@xDfa&dfbbS>oABGZtO%wA(We;Hn0R* z7#Ke{Z}B^X9mx@^B`&|Tn&j2ZUJE!Kt*2|LI}shaRDZ$k+|0_#61sQe#$@#;^#1NN zELn>wKh)2OL=_ddeb`SF^^r6K!xgsqE3^g+Mg+3?yJNtQrY*(zg55*yaNtwkKzNOm z7D&zXUPPiKII2D2U6a9u_D_!>I}%k&_=)zax2V9(V&H}nChp=8&0>)F1>iC+Uf$wX zfz}jQi%r-*uG5Cx)S~t|nTPQOz&gzXI-B0pHAEq&oxOnYUqZ_0y=*pAh6FT``7Ve$0~nrjw-V zjOH808}u+8{TQ${2XifsuSDO95#+=Oa0Qe@ZI(b%a?yzw=DP(^K5i@8>Z=;WA&EkA z4Xtxu5L(L^8Ew_cagMdrNxvY&cnZdjeW3=1VHgMbS9xz{fb#mJNn+7G5#eBznoU*J ztM$uAH?CsfB=f7rz;{1?Yyg44ieeXZ2QW?pz>$&eQ3)9fQE5)usfrS^)qRJlabZwB z{$LPO88xV(=_+rJ82-+pwD`_*aRcosWo5@sJU=!IMZ;Y-{Z01(9$;+1wus__l*Q2h znMNaBVv{vUCT!bc3YU5f{77Zj1q`ZWnp5&`XuP>>{O@tXF zow_X@!P+RvR5bhZN3s1J9nHspgSiaSk0ZA7e=2QAIi-J~c%JA}iTws zHl_Xd%9UGGUoHyl2XB4rRBX3nFd2ugmU1sA(4n1|`0;L(%OSzM9Ordt1lg~r$IPA@ z(ZZfA=wlN~4H%!5&et^@YTEc~e4M-?W4d4S9|(Us80kZa{DKkMh^BwFup=~3w%>Mk zQE{7y%?weMAqh9KAbQ`RSiFuda_{Lg>zalTKO>BW^RM|j3{{6me>ANTHu;mpB>dqD z<%j>YK7DtK^^y``>S4Z4B+8eMEHb&mSUEVrD(khXpit94NFny4fd%8{hUdQ3_XW8A zXyYM}cIII+#Vmk#u7QDp1$IRNBJ4m57_LZ>3M)`Nm2)x78qp75A1``u8w&i3HVo#@ zxArS4Kywn@eEwlhr|a;0XYhatu6H1FLk`9}ZWs3Q0k|3Bq5v>Dhh0aKn4&vmDWswH0sB7=8LvegLi52iAyI{)wY=ip z>we`}<|5inD(Pjs#CazV#S<6-%%2LdFy2%&UcP4p?VK&Zk~BL`s5~CbW{Q_W`amq$ z&R{fKC=#J}rR{@=g0e)-VW~u1!kq*M<%yf5r%smx%Hhe%_gY z2R35h7#f>Fn%TR5{~r)+;R>%^bttC4@Unu&rah2-56fXwRlXbh?w<)OU+L${gN)7Gm$c}iB z_Awv;i2_aHBi;%u1L=xjo7{q--vIDIle5eBt(@+X)rAe+n=)1B(xv)OBUcTJP@YA)+RPfjB@zbn>`{bh;O{yzNFC9*qYTiGFhsleFf0R?~6wg!8 z|7fS#JhqG9*r%{;Ovf@auC1W&(wQB@?tVRr@dl9K!yzRKS}r<}X6ZSzNfK;s)zy}i zm{>V+dhM^A;ZSoeHK_h|7rr3A&o6li1)MC8k^ls_H0cfR7 zxqtnqC*1(Xn!xo)Tx`C*=Bvi7Qa4aP5K$elvJ!GFY8eNRme7oH;ISNMCpgcL7*121 zQd$0aAh1UXD>f=$iyOQ?E}U6>fr1-2d>NjYDZ&ljZ$@Y(OTZGj5m}CclRKO{ss60x&vXB=`@!L=KzAo zoX1ixXj3_jO0FO_P!Ah?2(_dsk0Ww6jCSnWTLOvI*f$K7Or`D^r0Jisoy4gn5~v4h z3cgH9xry;(Qsdt@J&|(afNS)Tx>(p}KLeQMw5jRBy`!RGB=CZ!%k!TR>$G42FhoEc z5IITu!x@u`*+G=pXy6h64Zq1JAD}*OU*cIg;bGfW=Pw$k)~?qhsD7X`!BYBZf78~; zZjp@c6i~C2UH!+;?O|y0=Aa;7a6T3t{;SZ-AaDg*;@J~@;+-zV1`5rYcAK=1(fyr= z&)w>!chx7*21waMf2r?cwVlj(yobwc<)#KpJcr zO)jdK0nG9aar&ZFIuF2fkaOWgS^TiwYI(~sP|dcOZrfX{G#LFjH7(vRG&8LiqD!xB zdN~-NG|Zt{c8Ft%l?p)7*hw#S#e4<-ZYopC#IsvlX19mf9eC1M!;VGbP8N7(XBjhH z50V9lm8JW(DMViISJ#$OfjH?Z>d(Oqk-edrHkx@Ltvmw_rvH@M-iRlWi9wwAoVNPZ z7bp7v{q(e4n`G+q9IF!|?52xxo_w?OCa@8b^P+qOw{0uGk)Es?v{mj}^M>|Il124QdjV>on<|Y zYIu&NgVXbz1#<>$oQ!r)#imV0C^xS)$7C{iK*)yrtXE`PyQeIM{mi0*4<8<`ufWPC z8I^CWx5Hf65FkF3eHn zH4b!4!1~C$@C5cZ(}*`lJDUyoQ;*VRKa9@_-BznVJYJrBfds9y!UBX%SCZ-<>Tsai z@*LL9Y~R8PTTn?7EL?T4D_&_9HTF#TzFHjbfQ04dtHtKdF}b=WHd1!SZ@FM2A;>cu zuma;cvd7(K*!?@=r)_WaVXKwjxW=uj?ixH=6DOY_Ak_2_RtZGX?9lrI_G_y4 zZFkKy{l{8OoI!nZfp$cV#xNwsb1`__{qwINrqkz9Z#Pq!Cury3AYZ#orBkav?-zfZ zp!IqNO;J7Xd&uejWelN#iT`s$;+puocNby$(vex#NE*TO$vK@+K5p)k0c zu^sdCD0&_mMZRVNdSAj|&q45gNCs>0ghyp}b;dx90MGE$UY+JOQP^F5bH+#e+0p-t zK=uqGAjUNRk7Vc|q2bKsfYRW?wc7ko2-uUlU@4E9f7mTm zAlx9?z$VFvBtK4$GE_iNL*yt1LjJj$EmgYOpe)Xz@vVOOC@6&IAQ)t>k+%ANOaiK* zaQNyOHoQnvc*EBKZ!Bk@^m-ofe#QRN5OtIpEgwTxALXa60?_Pl#t-aIgYM{Mmc6E3 z1}oXJHWVA%SqALtZh`mOV?Xq~$+4=Ky-SOem~SXPFpTjNBG zh^9tNQKKoB>X6ieUIGLjiQ(m+_+c(&rnMLRO?r|VrkLjjsRDLO zcaQ{AEaPEJ6*~h;)k)(I=P^K=VT^Nrhv>r+^aG3K2b>#&>^5}(BO+PxeM0PjQv>)y zt9!2PNqyb{#VeI-PQGh+(8Y)#i_xlJXzF|gBoq5UJG8m2f#d$;sUt5+U)%uY=P`a^ zF9_QB?X9LF(b``dA8B~^E`42+ef-+|C%my(0N zv!-+Fbk=nL01nrbseY?e*ZLO%GuiITsKd?8xjqBu&Y)R9UOF<-2sacLJU?c8Af(SZ zjIaom$opqkS2b?ZetmJDd9+Vg+?fRVb$hCaIP~dL>(=-NpU024&RGJMtW0#Y7A6mD zu>PV|Q*&4tdwcQT_McTLW?G(w&vXpiO`b7DZd|{9r29Npwa(=IiTPZ;hBzmh+LQlu z=cJUNGEB1upc?nGwbf1+lCT|e`x^hK#|;DL>O9|e{Po2%os+M?vjWTFEDkxt6&v`~8uM`_n!hd( zUL4 zAAhf85K#@KZ_D%Vq0>BF^tj8Jkli=~V7txd`T@tVJ)4n*NPzS_8Ggn87~ESzgW1B@ zWJ0tKC8W_R6r+!Kv^+|b=d1x*_1nLUgZg-F*HL6~y%G+-~t=?4zUc`<6q#+i%!njB@T-~OG4Pu_DZma z6h5{9CbbWkR7Co>T|i@EM|DsB=dzSiUcAJU4Km<0JsRmmtE9$~Lx5#w>1F>yS9Zwx zagtaA*lOQl7S~3RlWUQImSNH3XO9h#vQ@0oKX7xAT{Q1JV-vLfAXxr}fGkBeh_}a?<<*I$6NR@G z7>_=b18y8{0qp^;1t%&tcMpGDTe{sc_x93F;EFy#1OlOz+S5CKfCFP$RsE0;vb}ez zv`O_QjJ?C_dxnULvHL>vi@)E0J+6B4HMyTq2kB9dnRG6vBK!-c3lsI46Ax>NSN@rK36p;CQPL>9O&tTran z^TGVAn>D|{mX^1J%|6vXUcrq4K*TxvSO~xrN|g!_gJ;oRs1gq+w zSYFjD&gX()=k_)g=v~1*E}&Qz$T|-ZN)R(|TFG~6t3T$aATA}AMa@wAr&{O9pNEYn ziE2j0brX{p0GC$)`ZwT>-&NH%cLN?v|A#FG260`d@j%Oo9LQ>eKsJ+2R1}ow&Vtwz zf*(v6einvI9FR;XT4hlH{y(TxbIKuKjgy?Z1@?}5{C(er?7`^<7ZZR>$! zGgy(Z)KhuKIN8w{M2wj6LwG|F`gcbm!42Xm3xOCLXTx{6O}y|IYXm5hd=o4h4YXrm z%gWrwu5v^Aa0lTy9S)iu9^0Z1_(!ucB0QYNYRuY8HW-2a0Yp`@Ao66M>#>1c5*|l> z(DBEYE9W!g>_Vm>~)D88S zMH>Yu055L<+GF-WM0iIq7wg!sBt?fJOHtzP6>1Q;(7<1ZO zyto8-uVxGKkAvWMByob^p92puI;hzn* zvfJ1L(DKrCL3vJuKC+s`VN)n$uVlP~qfJZ(+j^+)>^hyGc~pW3NX*PTy-sIT%+9y# zM_0O`O_vhUOxkxruq8B7CBC0V5uwN#dkMjqa^n#vMU|)%8hNyaSNDkwoh0Y!jma zZZ3S1NA(4hFzm-lAjMMC3yfDx%saG`zK2Pnoc$eq{6RpSXlMcgK^x4oFbx7#v-VBrbE-K%>8NQNQqg-F^8|?Z z7j+k5!H0GalML|MYo_jxYld_~g7nK3bb~Digyr403Y-9Ro?jXV{v*(l1HHpVgi5+X3Xsa!g@q=1n0dVsQ#}rK@%0PU+*D$oUsmchn531Tn-VxRmecU;e?PJ zbOFu+Y0FT}I zdhzWhH6TuZ`?33gd9Lq$NIM7U>m3$}7`5G{u=F*e+Q1Fp0f)1k+UK`qKRHT!^@yPW zFnO#5Pk-N6N33t;B7v--KTb3&g)85|p(@-i93ioF*~Y;w09HPKnZz#C6X}7yhl0rs z%vdNfGbtJ1I57D3#xCjN1#b~Fg|9IVZw3th9tJUd#+z8KFBjxTRGAH)*Dz57!%v9n%{@6m%0yYPdNacRDsI^dsUX#Sl#p||< zPYdau_%7yC3UT4U;6#>|1Zpz80Q;=s5&^^$RMa^G&*zsEvuw*z92ulR3jT_BM6YV!ty2pSGCsTW z3(31y6k3MsVoN7#>yEQnYP(Pl0Xw2#?Ugk+fYlk5_`30;l|B?R92#l z8iF?Ae=!$ukErM!)JfezP2Mi`=NcGZt8>q+S|Xtmj}12!135(3pGC|GJBly(EJ%#EH5C&~@fh;33QL`LPO zK-Y8!A7egRIl#i>MltjtK=DIp4SENphs`-WQsWusC1f@wLubF)g+^Opy&TlisO~OmA1Ri@J{U)ZI9K+xUT?A^KPe#nGO zBt7r@#8ri6(IbPIG!`#(q=m@tc=fbU#Kt~jYRcobik}Zz6?J&(cOvE5-ur-b?*c~I zr6)Idn;`|gsdn{d>e8N#2itR}ki@$aj}b*NdPPuNK8Sc!Ussa{3Z@A__-|S1h6Y7e zWJ_LX*n@Pt^9Y*#xof^Azgk=>mYG&)I=}lzdTl|*iCd)?Udj_=x6KI|v4a0@M6e5* zqHzSyPr^+kjj%cK))|3Kw2D*^2*)IFIm>;!k?}K+z6@{q!EbD=i zXBtGImCy^MM2-$NjKpnVPBgUg`}t>RC|P+2TY?|GrI^Y9fl->D{tnFkp{9Jl!E7O@ zJ%FmD2=QUr%MK0{8&i~y&BKj;naxPYb^!HS5m{USIBJp2(v68XUO|vc5v|V0mP3#M zS@~Zee`6vyKLAs1OhAg>qiZ=gWBwz%2fKj<9!LI0?e6sCv<;~qM5YY2&S+av%6=Os zmh);>J)dw08TFQ%&$SmZ*^EZc%ydQ-XEzF5n4tYQfAhwVPRK~|ktzLbv~50vh6+3F zwF03)nV0kHtR)2j)B$&*Kkm1})zU|GFBiw3$6034Mo^pO05L#DVHJ9V(Y3K3SQCbg zqrWpvqiFPB?2~e!g?q%sHwg%k;;fx^nC<%7)l2Pdl}bRUrQlTcRM>i_6* zwZ+0!u49yeel5ieow*BVHk(0Ak=E8$2|hMIG%t3O=rXh>o}w`bx6D!kN<_$C2gGz1 z12frnij~bF5r{t+IR9H_Yu^piF+|#CPyq0ao)F~-{ffBdGpGyW5+M@rS&CxQ zxg;rpmo)2x<36;@jY{w>Ew5KWB^KBpYc?Q?M*g+$xdh>C4g)WsC=k?4aus&k%-?r1 z7GLufHO1K=>H??VcE1@T6(MK`;j~4LV{N|Z$I8><;{uFS5220w*frmPAVPXu7-6Je z?V*hyRalCi-=()oO0J_)9@5`g3{q{unOyJd_PrNL?RvB}le#AiSDW>v4~tI5>)eB? z2O))vtT2gSW5{TpOV(nnkP~$EPNOur$8LabkVG7iu$@CxO&lCh$|1yk1^3iyed!Lg z&(K1`#N?Pes3Ol|ppwGcC=UO*qdZ82Qj5k6(oAF?B8XrHUXs=t{qb7S{o@U2$E3TN zWeoAWm!Q@`pM8+m+YOU8dcjOzz#14rgcP`eLru$Xe_!|S?}ZOtuyV(38ea%>dM4_Y z9_xw+4~`YPq%GDBIrtZAwEXzYr;Nd!{Dt-|U;0-YxSp|O>|3fQyVSd(tVml3(9n*= z3OQL`$-o9>9^V+}l*h&)>lBpS*426%@I78&68F%@)Ho!>^~)dg=P#OFvb z|2%W^>%#b2>tgGm{;7t-j=7m}LH%~6Q)@3BO!;)y9i>2FeSQ5-yd#@>v((9=O7I)TGB*>wa$%QEX*-3t7U zYt2is=6Pau;d4~+(-9F7$%XjatqX*Og`-})FzFSj5f9Ryg}*Xliym}(>Zz?+meB0x z0J^eP+TZx~!INR-pSt5R z_{94&Edir@A(?4=dTsGXP)Ia?KVjK;X5M^UuEzDANYkGDwU$RcN4vVZEOOaIn6e`c zmZft?6(xtO?yeOTSg~S79lDYe!GA(q@NVQR1!k#@KJXK%R-JT9FHdgUwtT^j8osmI z$vsobO%99kcW1i?1*@_~n*y#a7TX$dA>9~Sf>b4B7R+XscsMmu&yVP(6xgdEbf1|8 z^aY40_=n3p^QX*q9v(Hj-r=#*b-2E3O`=${yxqN}_&O;;KQeu-(@^>FK3`^9tB1S? zvm?mif>YUMS(`@d+9#L4-9cxLVq99gzp&O0y1OI+))N~Y!uqETmWX>YbtgX8cGaV; zZm{eC`)1kud%oS4Y4J&JbQ*0meJV%4qwe69Y(p>QrBnuPRYg_Rd!5ZEE-mvcYo`h9 zs|4`chLH7RPKvS61&lD1^;o5I`OLlQ@%{xj_Ghk3+XEh}&>mr0;ir%P%D{j2@@S4M z2xSlM@?c{0fUL)sYkaa5$O=!KJZS*v*-yk=N6fZw@=^B=hj{}z0edc;MLOpT3bAl{ zEf5DUU*E`xh%K4a{I#Z9!O7u1Zf#t9$$gaBDtc&8wLe}5S#7;m}^ z(|Dj+yJ9FO%cT|)|4ezMeF%G)b$8G-aXSPEjY!Diw zD3tCV`>2*(@udl2rzaCxw|d_%L@=1ZC(5(J#(43K{agT0<*lu)J-Khh6cupsbXBgk zj4^Yn@zM0T;RUQC7`eB2C!NEsnw#fzu%$lM&k-I4HkM+s8GmV-GyHRPmxnxGfo;x2 zqvJ?oa51)D{g);Mkw!0&7R103a##?n1jw;*J! zj57pL<@nzg#rZ+RKUigYL-Z&rTbE-x<_x}9a{4+eEHOSNB;^L*$ObK znFWZ~eZ;e0yx1;V^C5~)Rsgz5Oj*RA3FR_tVjl?`uSXtOEVIVU3wb?4G)Y7>*(`H& zEJ;p_mX4v$In}K@^D(hhDuB>iB&zrNz@ZY66#z+QOTL<_sy>#k^K)`H$HI&q4#^dB z*PMSTQxiJIJCR}9mDNPoc)y=zwlv?OMitB)o9vLL2?s*y(%`G2t%47Y+#j$89E~pR zeuf^*`U@Fmg)7Hhu!T)bO+A^wv2{0@R!sU#HCETuGSt#vJXtr)`tf)pMSh5wf62--eK$gueNX77L~J-b*|jJ z%_DJ@(`*hsukr=#23m&@ib$1Jc%d;(Qr6<_oK>yWYPzxJ$X*=d(z{;Zl(fHYxo`T@ zUc=UL59G>ogvSTGL?KXR&A*zEX*k`h0!M%^-i(JKXZ3?%eLjaJ9;f}&xd&!`SMeRj(+_*< z=tqopC(CzfW?c8KP_~vDu2Mk>8Z}2*=K;^)qz|k+=@#c^z4qdWm?VYlv3J6L*%vRE z`X-!`thX(w&fu)wXo<97c(_)i>GC<}U%8wqAH?Vrx&>L*?Wu->?j8|ywNhg^l-D@K z4HkvGvL0mH<1+{3f-n}!alA74LK~#7@YuXM=l;Zh%N9W?BO8MZ1?g-|w@6QKlFjwh zp&v0?&srHq@t2!xE*$Z29FEp4R?8W$W){p$_s_tMZIK-)5-dfDzIBt5ecI$ep$ZUb zyuGY_6KG?!s5*W)9BDK8Lz~aWk#sx%Ia~s;@ji9S@fmM4kFfUVY2mDy>4`KrtO|6@ zuC;4@w0h_1z2wK~)-Vx3*Fz6Bwhq@AtWr_ZN2Z`qV%c&}WQ(km`W9Iohu-9#%)yeh zvGxEJ4Gq5o?2Wjq1S?&BVBhJs z+;{;oU^Sjhkwz<(P*IAgAdN4ebs=kD!i0s{HV#=zOH9edRE9#C^CXmFn1Kt$P=dBQ2Aj& zI^*57{JP^6z7}0kk?lTO7YYuo()PkElr2iubdjgVzqefUcT`nNa;AslnFXftSw1+q zu?5cX54815q$YZhX-dvpdvPxc9DxuH--CBxdm*jO2R>57p7%J_tC(nsH}D+fi>LVE zwDuNyD>0rhY#4J@_RZ23VaDdon|Tuys(;Y6;+^G!~@)RTFE*6g|_^GC2r*DIaE0eV@Q))O7 zo`E)>x0z=RpIi@9KAG<_dDwX{pgD~ETLL$3!u9Ni--|DXfrluSr+#R_W7HXi-t%B0 zoxb26m|cOg|4P4An##Q%>8G+LF2!o+EvV*;lGiW}sP=EcXb7c&^2qcZgWmCun|)7N z$X>uGjma*x968L9xDQ6?Wa-SLl1D2Vs+Hh}JkLQ_f^~X3o>(f<9g6q8-b5$ad1j*7 z5S@x+#pceH--i#=8|r37ho=KiC@Uw(=*+5mb?$IH$5nQXMn-{u#ainot4f8m&WM1M zFZvb5d%gBQD|>2&b8KFfB7x&zWD%=7`E8Y~M=vz$*2H!9JJlR4fMZ3lv` zClgjTz5>dTC*zP0Fisc$$Bwz1y?i@Y6x&t+$RP-%dWEL%g}*%DnZT zK5r&x9drEm$ufO~sP2?i`Jq^CGKp_ALz9yKC&zL1{_WZE4aVN`CBGj6Uq3Zs#d7r5 zjAKH-O$U9wmE-qC>FfWeyou|#X>>z?#WCsS<3rZWp)bwQc{HOr{Mc5Z?=yIVA{>KX zgSBny^wUfg0>y+Oa`9nHzW1#TDId^56O3X^$`H%!)NI#}r5J$mvhTUJhu4a? z@ijuWft1X6gdr4N#;sViOiAJqO6(HsQol~qzLA*{sXw0xFTGwNMWijcR;Ms7qt72H zLp!o2JxY97HcttzSpwqtWTGpfv(R3@1RwK8^XTsbwOc>DV6;!p;AFDR=EBr7`K|vR z2(T@I1B-jx0UOXxxfX9~g9nA3!4nM~91YFq9!PTB8{3+{-r+oAUO&)8L?J(M)C19F zJ4}92@bpmRBqFrZ2hKyG)eRIi?7j5+E%;X#z0ez9p8ueW#GN1pg1K$ezu`o3*=ECmg}czayb)mnGy+8wn0 zjyc+VI&+IzfR?5-%|ZN#q^!q*#iC*5(Qdmx8-@0ybp-iII@nOiT&+0|zwlK3_fAN1 z1!lfkHq=$lgLdiITXyG%<~a4D5RVz`ke(5x+`}RWEy+@{WMj7U^P9SSy|Kza6qGMy zv_XkzOk^Ml0vcufroiu`&^Ru(d-y!P$d&4+?R0c!NyNC(POSl$UXqETrI#34&MS zsv5Gy)1x`11_5R#n^RyB^=_@RF)A~MK`aJuEXZl3j{AY+hUZL&-|qtqIQNV%Rj_5r zWwm~Sdtu7w3gcpo?%AxE&$ZeL8E@>!TA;Q^Ju(sOvV8_8BLF=~yTN5KvuH>d0plRn zhK+vd{nP)P^$qk=J;G4OV*8wmke^u~SSnH3`dYu2;{Syw=}o^J`MvYh7qa}0$D-8& zVf!ZD&cmvt!FePi{hd47grG(OZXP*^DR~58m96;Iy*$fUrbGPzU)E4XCSo$S&+xkx z#7~ab=74G@t0>kPHh0t(jYcthD#~^wJ zV`F|Bsv6!kHm5EH*HrlrYuk5*v&#

jVOdL1TEW($erE0$|suuXh-Ci1jbfXRadvYTc#Alp3W>- zsdfI5Wr-+va&q!&Q42lyTGm;>Mc(AA44v^s1lDjdmyNO%$RoT`KGz4M$1GTScy0Oj zri2t^MHypmlqu1lry9$rOz%$P_#2`uSqFoz3@n8b0Gy>_IQzu+E43(mFM`KXZgxR> zj(Uuo2g^hVH6wiVXA^1I30g-9h{KgAUI?fB4~!?JS$<2KwiQzrc#R_t@w#9HMVAObw|=|LG(#Ow z3OR}wX<}j`0IUkgM_;3OK}Dr0z@O^x&iD?vsD4a8aoY<(f6_Y$21SCP_ z!OeAQ+imkr{(SmHMNe%=k6C^fBG{0 z{%e*C11&Y?M?~>k{dX4_Np8M@aP0G;nEMo2i6YqZU=j#dBGw%T z=b;uz6%9c2s)NC*1d;^;Y;u9#^p29MfL^FoA7)uQ`7_dXS+|Yxu>tY zle+*+G>%5yni1>?L^og0<i;yn(4h@-I|l?acj zoak&I%d$S%(?%N4+nRaBf9lsY{Qe!pbL-rsdJ^D$n6h|DWQ}D;4FTu1UR$OO zKgf&cL#HVTqF@7F0K1KkS9LdiXFXa0|CM;F>G)xdd66R=zBWx6xChXo#l~Mh;(eZ8mlU>T8?SjUrd|7W zOOT7)!Il6u$X!YjKMzv;L7^C>_uUiK)zyTBt(LU6B4Ar`2A zdXMhNkr$D>weA(ig(xMIwiFAf!b^GxiM=KCiO?%$Ev*0;BXNo)vCDNGFVRVXc8@f> z#E-qZ5z#UW@aCSBA}zFodlNvrR8M+$99uiaEV#fzG_=a@;>^zGI$n z7HW2aQl#dY>CrkImqfgz@bq*sip5ZfwfOfh?y7m(9(pLirF{=^w}68%#A+3RU+fF4 z22N)eK;IJh+LX`x$e5{{t12n1JbIybyJj{0P`p#G>1tt<6Me{K$6prGK46Y@D!FKK zTFH7KL>7=9FMlwAgiMg^|6ru_RFrBLB_~_wb{EC>gKV-Lx`o#Ch!T@Oa_s;eP)K>o zw$VBbvFDm$>E5JDvL!4J51Z^WkJE9dcyE$QAB+p-LB=--17LJ}=*OR+S)QIufUVAi z@Gw`4Ss8^+!zm+s#AYbP6W1JlL_dAVC~!~RW)1uEKmOu=@^G2ro-^V3Yb`!-u`tb- z+wl{poiKeZlPOCoN;{n9d=`LVHmk}3G9&8&w~{&Vkq6@*sHkK!nUEZKYmsB^66^!3S=hMgxKGzO4ufcofj+?-_P{Q2`faJlH zMW8qsrR20-)&eo4Fb9r#ZH+F!_zD9Se15lZ^5H)9 zzwx`g-=(wYDBys{wV=^CIMF;K6=0U4;Z#2gAy#nCC1|(qhL>eQh^SQyAfTBXNzG9} z_D;@0^Yf2UPri(vgt@_>1wH}Qbq^)$*@!0E9hxU}C*u@bf z6kvpM7gTs^os8{JOX~t9PQT%W}) z>0)Oo){`ZemLqT@ayKkEizbSofN zi)#)pNQnpm>4C*#`>cEU>kUUFwTegY?rk>O6Jrh-$MEeM;|jf|fX;~W`2At@m?*gqY-d?o+KyPSa! zrCc1P%*52lVl+UL8V=^cF#^E zCloFZ0-lOQ{XPw;C585L&rvo$`72+VG?){Bn}#E)w8JolL4wOsfx!PQCjWBsGu1Pf z$9Hk}m45bp zh8Q-m0#6SaX*aEE-M$w&7sQPwiuUxc(HMFVnGj~8^HHQ;Y>i`EeW%r%N3tF_TwI0@ zIHk%PXN<_A0PK-eZ_q(EFRMH?6pGs4nc?5NvDi>n0p2fG`@I`)l&oB#nuGuTXFpzz z+v1D^b!^M$NCd({&UZ@v96lU*z{q$AGb+8`WlfVqXD8i3l;416deB81lV<-DF7XPZ zL2TnGo!Q5ytb40OG$YhGwwm>lVRW@VBKYT#n%ifiC=oN-CEuRY{Kc{(_pn~*BNsXR zw<@(bZRFpoqXAx@BilcC=4m1L&o(im|#-fE`l zgO`_2#J9@ef~)y`3XMg!9_i^Pq=%2m@rc!o@>cO(*r{~p=e%I)h6g?^|5Ly}ou!g; zshK@aii|7Xo#m}4?ZOUM4AN$dfAT?X_xu(=_{OR{6Cvhem z#)D;;N4(UN^$5fNm^3>jPV3a=1w>3KI#j#pNPNAqLqlD=+4ie+P)v^6tCo3FMxwt< zKXi#0TQN?@6YZxa3YWiVkGPOmZ`bb|8J^p8OYiL_+erL>m{`8XiZiuS$`#oMK8`zX zOjgk?^%(0n%uCCU7(A07*2QldK2m%~e&RwI?>gy;@bNK2-Kol=vBpwQX5eBeG0Bpg z7t>AOWS1!mc6u{qi(A#Y>P=xkRN0#YncMnv4t*ZstV(d1e@O0svRU>1q|O>a$&#t~ zqtdnqmDK|5QguD?@8G^$11WVsZmC5israIn8hpyrHrvu@f$CtwmvRUCmt&~+(sZ?P zWq7w-thR7_%J_z&TfC0rsk3|h-Pv5Z`Z&0g@=-48wVJDX{eB_qX zEkn=a?2)p$y}p-xl(SJw}0@?(66uNYA?)d&4WDS3Pt_e zLi=8wRs4U2SYO^Oe!OcF<;kGlcu{>Hu>qK~1#oS3dNULlQV9f9R1ehg_0YkIpx&Y8 zVm0dtZCGB&RhU&(fz9l?CMX8LXgpINqi4`AoR4jz3k;DEdQXFbMgQh?3;+ z38~Sv(l(KtF`Rn3Btuh8!<3b|cDrM$eQRRy)Oq9~mK}k%gbkQooQaAIGWlT#@)7|w z6)=m4?OX~7X=U2Ad8<*n)s|GLSZF9cQFk7wtm%0c%v1LH_X?V_j1qyeA?|!8%MmFC z+BT)V?SLS61gsW!(B-p4_#ooX6J+;Mv9W~hXl3?22tB4%`FV4U7r+#RByW}Amyr-9 zVmaD@-)IAf3f+a6i0YNBItMcMI-rD99d1K!=*)-YX@FGiFLM_cqVD+^uygH<_;c`d ze6r}lm%DrHS7}Z!+c6iB3X^h07D?x+DAKxcU~aM=sBlE$qOqsS)6ZzT+a70tG14&-L$?D8od7&o==CV7i4jQmK&ID4jFI%wI<_Zx0RU%$iB8#+_6_eI+uGv%-CAkZ5Hq*@Si$ZeJY zJ2};ygH=Q2x)f+Dl>>WlMGa_yqF=+8LOgs53wD<2Zn)fX4;MP8j8qWtQ5b*(@^XT> z-}A_J>6QUp`mU&%xHP?BoJD2;q@7cIRUZ8ohUn_5EUTBn{2K`1?}hmcVyjz z>8oRJOqD=Ha2+Uld_d6c!?Xj14+)*)7D5CqQC1b&Q(y@3G?GVB!LymhrEO{cl_%oC z1nIp5)qnsvJyF9TI|BkLg%dUKJgz5yN9Lw)P(}7)61skqoihlWs>HWb;AM!{p0ZS+ z(j8OQsVp49XQ8Cz^Zy-`Y2Q?`OnwkibdQm#i4i2k&_DFKPKBkL|RR&Lc5vxkp0%vK9 z>~!<@&*2JrxJn4h8zXOfF zB`AUfQ1t=ix%vg8Nj)>!s3;F}R}$suf3f!-U{RiF+c4Q|G;4{QjlGdFf&zjfC5V8= zL>&;Yf`EWV5Rn$C(n*ZEF+p@_O4UfQP?RclEKwY~^gb$0x(-bkX68Sy$FkXXci(rv z@A$s=_>cGh=P+tyW}bPTxu5&GuJbz2^MdS>&A~Tv7>HM3*Ht{=Gn!(3UjQrDf?v@O4PD#caMLSd0#Or7k%+(gRX3Kh~{lhQQwiq;o z(vAhq-fh~J#ha+#qIYzDeSKcaloo3Jc<_ew&zgVCBw_pBhXFw}iy;btP{ec~3tv4@ zIt<*2u*YDcZ_w&xh#P2BSNx4@6jk!o8hziKO?}M=PcNeq1R#(qo3a}ld^c7U!HNqnZBJ>%sv$Xp@8jIvkZafCS#dkW?Pa3aP zs*XyvvM>&r8#XSlo0R1y@*T!)0UFSCU2yU_EHFAP2d^#h+0Zw+uBO6oES+{QHfN?% z{u-NTH=`56t;;>F91BM4mb%sK%mI$Qj_P>Su|43*`R;qhAHYgwvNGVCR#cJ1=NgSw z_+s(AZWW?(Y~333M9_86EVVXutz2KNdSu(guJqE(eyfm#LJN~I55GsBo$kO+TnsYX zUda-Dh{di-W?!?$r8So{8!P3%_)27}1$4uo?p*nl+n>uuyY9N;MrBwF;8_i@wq3CC z`CAo8cMnktTjypc^jKJ;((SRxF1U6=yHcGPEjOnr%-pu(?;$WQ40JlnO@OOe^}I|^ zqV1UOTvj8{-e3i-lh%!o^8t@Ks&s*waFr{5Awr?7;8A+IX!VdbbFp6+fOP@bLRXkT z*=GIp;@q!Ok(a9}q2X3ID%tWOgR2)iOoKU}6O zxcieL9R5eY4@dtcYo`Q|^}2O7<=asO(2f}<7a2--A|4^3q1JSt;$nt_(#B9pn10d_ zj^@Yd0Qg>>>Nk1=b+#sfIRf18DwK}{M-5}mwor0K9uAaMo6zNtv_>kYhGQkA*mjix z$M`&*X?et%+Ix3$OS3=4pEsvLxvh?sL6=)}oTVww?ufYek_P`t25C6aO8puU=8v-MSEwo0Byhz!$37(4hxu!aHUb}@Ye20eynv`1X+So!pPk*e7md8qIesrrOmm?6nAnN< zNg|An10NDGmU_o$fp|_EK!T>n!OWA4_+n}l@4LZNP|=*4w-vTjNM4dl_N&aIxr8T zKt>CEs5Rc=GSMNcb1^M#PGRvxJy#s6gvEb5Gbl=BQqze2)Qp%+wq2b% zT1ocU9%1VF9bqb9R&Bs`MAbE+(=Et?ZP81;ax5{%>nJr-2$hBp^42z-F-D?jgY!d| zCJp<1#yv;+Momw@K8pQ}Ox4B?Zc&!|BKbX6li6&OiN%)L=youSFL1BA(EZ$w-2|q? z=b+)$p>!O}yr$ze-K#m^AX2gh9)|`U5PTY?PC}oMRB{iiQ>Ii|{HC>7z4$%O-pBjh z=Fd18RLjL}kprwyXKX!zYiFUY3DQFQL8|WRbvCcyeeK+en>NA4152{>C3?y))p_3S z!QZS4&I)Cp_G6E0aR&Sgp1E00?G&U+r{3mU__Mt}GC!vCns-F!C$+Kf_|wvkP)k*N zGnGR<75&`Q{SDUEq4SHBdTxIEj=`~UxL4=%9!cQCf#_*|--#X$X@8g-;%1;GF{m1- zX4^Jf-R-b?_=S9SU4Rp7$!xn8?a{i9%#tt*kLP)=j&*c(J8h? zeU0Z}N8IGVoI&n*z134Qy;S*0R<-oD!6g|Pl31Z^MQlt>srl$Ae^GF$sJp5!t(+I| ziIJ~j-TL2s6jPNiu0_Wxn1q!2ul5`ebbLP$?3tr?&{HbayvAmv8=|WWg zWM9Wqt+4W2aux+0C;HwilI=7%@{~{VWxg`0x=+w*%GAS$JP>Wf*)w=^V)yQf5&C(jkXvM%*qn^<)tZBgB~7Tf6VP1*spU*v|hrK=|rZ(nPQ%v>~jT4I1zbIHWDKYW&V0^<4m z!BttjA7hn7Q-ck0jt8p}eDC`;TSVDUHOAKuTk6T|zdbS;?H!z4Aw2GuKY2M|`;d}C z^3QH(oD%5b&cmI`VAU+JtnE6F?%P`;9z@$!mP*z;5!Jrgb5dqz@}8NnPy5fj8@Iu>S*!88p!n2Ft0k?9yk6muSR0tB)+#uZ?oLb zZF&}5!3lBX^3l7aKkMj+OP6O);wXw zieVX+i{#=jOWJNq5-i!1%L@c{hB<3ZMg}FxnI>tx7eW07FaN?o14gExgb){8a0p1ThJYGjTW*D7d_H}y3O|R>TGBahe-RH?C{%UAv zeCMcOF*R$M$G=qhyk@J>bN(Ao@c%H^?tkmk2fBUUPXrMxGllVPBolVdl=p`Ng3$m* zOjE_2iF%BNy4`JJlraw@BYY>7QH3u~D#ZZ^32Y*Y^3?&*wF#Qp4Wa}jt2$JJf@=ZY z!G#v&{EIN{eeur}gQt)PIPV^T-(2<_fhlDO5 z%l_`0H<0pC==;s?7wr^1#T*vf8JS}p^)9Jf`;#o$wsdtw*rnJe3*J6|-`P>ce$vvM zUhaiz`nQj_s^oH$29W1iTwy55^h79`YV3X?K@ri%h*?CW5~!aNQE$wiq@D@(0bTU& ztB~(*aRFC~#TML~sg!h{;eOG3?DOo~)6n_DOvNKFCVp90{s>!VII{cHLK;)buCfA{ zMGbfK<7rzV9SnMD#Jnd_p5p`BN6ye4IhcJ7ZRTxBi@rY z>n6RgCnxKhG_D`LFoRAmBf#yM>K3Y;QlMt0wmlh;P%=%03mB;k7Yf*2-FWoh=p3m) z$=FpL3v%f#=#de_`RekHBLknC7DIgL49q4D1W{rZ%gM<(SL>^HA`7D&GzN)MKJOUz zU`mmd(ImBpisweYIPDWl>SFDU!S=gPNa8$#FB00d4z#NbZI4l%(j}~7i)-XK+Lidt#F$K0$#J8bVb`@Zh$uU^) zcHXcQ9Yx0O{QUB1AUE2h<5aIuLe0n~Q83kWdyl~Ljnd~`&@7sJkQB zkUjxhUteD|y2vK#JZb}@q{FpDC8nCdE}U|3sTa5A`Zjc8UAhtpal=GM@I*6T90q8Q zP}@c@7KR@v`K=|$1FLRvVBScTUOJeS-GqMA!HM($Zw8H_{9!_|Gw&H7zY&e}a@(9h zu+PfPZJO-+>60#8*Js`ti*MIXOEzb|wmWjG3Z1MZWdy(jRQWdaA@SukK%b1HD*eeJ zpClV6%q4G<`XOL{*y4Z(QcT@X!hOz-w#s<9NwPT z2MhLBJ{hKa2q8)dO&ts0W(bftalA`<9I%*3H^EdAh~ICKBWdxA)3oFG^3lct zE5r5q&6WuY%q)Gs9A^LMyZ$Cf)$J|uvm3-rDf>=cT3JNGBVfaG)N-i%#@3iffdIF1 zmDlBj!^a$7-Tz<_wgSR>s2NtLc&%-6YL+Upprzm4dGA_MQYpaLP&D@5(bKS;yTNvF za^8>7OCgRbtw_WaSGD%rejM(Aj7 zr2df4wak(u?l*=DLW0flic6sbpp`e;CPtGUl{P<+Lf$aUZ?+smbYiF7Gp5Ug>g8*u z=&Jj}J90Uj15Snv8{Pm-RuY5?xMs@&BA>^UDhCQaXCj*2_iyNT7*^* z1qV**fC9nh0HSs;oA)OiM9S_x2G>7gO+r-SZm||ATW#Y|>j)RXiPHgO%|WFL-6}l} zv9Ii|;#woNYrcWI)&L%FIaag`3IMxGZQH82rGBrTLRDXU`SkJ#IC9qTH31I=vli_C zZWE!vlVk9S>jvWl5>RVm0I8-k-|9ZScXIFJXsHlG${sXw`Y`Aol5~w=CKFiCQs0jR zkU$J63Ur3-RtDqP9t26k_0r%&L=t&K#Me4g>ppvufJ>SF#O%UJ?@^z{rHU=v3Cq(` z;-FK$M`A_bM6tvQ(ddZ_2CewMQej3J~Ytp{;Am|lsf`#OrGb?1eRpI z*I!gXa#oWZl!y=H0s#>oA|a}p(gLc}`yWma!6_aD;9<8dxOP~q?&ELj=3lt!q^qw^6s={ne=L5dU((I)XW_!kgcU^jxg z(665t^ZQ>$)`o~ro`OGdsCbme^P>8=$TvvK2zUybd@_% zsFYt2#ypB!B~~AfP(zQ2w3{Tq9@y3E368k91UzUxyxBPMcdJ8E&~DuL?Tr-1#s;BZZdLEi5->$bnhyEotxZB`TY_0+n1OUV zm_>SE%CV@e_}Y!4I|(rtd23asoiRo9eeXAgWU)_wWWGzWKW$4*je>!vNpnc(ywUbg zX$9U~E2I!Pfw6l0f0h)i<%Tzz~26BjK z0ku0<&W)Jzgt&vlVkpOEfQ^D#WLM+C_r>RI^Rm05;e^f}i}%^-rD=!NXkpOfj;=gT z@%sAzJUOV7V@OGOen96wrTdB$d{-q#aex9Tp{wIdWDufZ5b^;cGYPc_j&i@q90R{i zMUEGcz&q*Q)J+kv58(Z8Gt`ZtQZm39o}k)9tgFK;})lO!7>?lzZ2GdZ zTErP1GaPIFwx>HHH{PYxGUM53gp1;lzjcys&d(D~bXX3_Xsur{-Lcj+mdm}EujlLOOS9za^A8=V9Gl!e?53di4P?67h*%Qa&*?P+^Yf@_+I+?9~yDiDP zD$Bmdjm=iRYEvx=OVsH8A&gO|GauOQazt(ihs(;;#W>#)=eYWHPl8@k_m#R%aQ<5Po@UjF$wckkp?eW?QcA?D|McQ%#Z0?@e7f@P zZkE4~pY83Ak;`n)$e=-f;naz~rWLb?YU_NjS~i}kcP!7jv?Ak?P62hbNiGJSIk_*9 z?*a8dfSiWI$0g}Fg^GReZ{X6Ob~hSla#oy-*GUv#>(QQ5kUP>XznK%bUVUlvfs|hG zO9#`QWk%8^F8hl+k-vy}0Uw7|6#7WYDI?6pBqHh-19!bZV%kQCWg-tz8M<#(j3w<# zZnasFEDw=O&uHJ!il2`LyIbqsSC)8XALOcPEXeq=c19irC2I7#JeX%pdm;L{Yz``$ z5e!47-5&xoG9IFPh?0Ji4x(8rM;ST^*P}4Tr4bo@r1d%+wsLMoD*o7btEYB_hF|!@ zfr{A$94-%wUv zCw;_=|2@xkbGQYFrA4tb>*Dr)l(!#NDpYt66Y(x7Nr4n(E+BO<$PmeL(a%y_JPe9I zlc$t-mo+31K?v>e^D^5F9-!8!YaWGt8 ze#zLxpf=Cb-{86(M^;uzWRj5NYI`Amciz0z)`X0St{=iGUo9yJCEkSfh&jTe(jr|3$8M_!_gTpDB|&O*Lx3-f^hW)DhJ8ALL?nFb)7ZPe-7vVZ}g?Jla$cPAA%iTzPYtAYc$e>eFv-6=|wiLsOh(}f{wfT{4}{NKSf z=;X&DZTx~vW|qTM41lr>c@7|$nZ_}o?mjbBy9R6MoEZBg(#mfVV)u`x#FKXZLQq?6 zAdi}WK}SoJ9!}KAI{nVAzub z7AZUje4q5suLKWae$F9^qqBfo3@IwPRoXCcA6pT-XLpu@Fie$~$qv z55#A54fh3srbL3~2?$&OJ_tI&xkxDon>U1|@$9dR4$&QFscz;#N7*qeLbYFj?hNSy z2@&XRrpy&5riF}geo>{n8@UJ>Fa#OLH#VHzk|uU*kb+Cc7s9HK+Nqc@UD zpA+xz|6e4huM^cX)`sAh(3_B184Go~M?_LKZrf3BFWe3h{pn!z$+F3M2(2iUTuX43 zND$0chL8;13e{dxz2KB(OeSY;W|E?MU{~;&yTZ$~UDQ|XZwTr#HJF1B8ONF2SEN)H zg096G7&@ScLr;|2Xz%75fn^4ITqnvKr?>0l;cy>Rn=wQpBQYLj!n;&ukfALLYbM|u zwN<(9N#_20wgazUuBC3^**IP4sLA|&CNWq|^TurXg`yckr0kw2`D+zs7HqVW!`^VHI`>b;4grm!JD=vofF=nQG0xGru=%yE#TH#sF5q`ID*R z!?j;j=zDrL*D2Z-?AV?DrTWymj@fAYq zT+So?DK58Ba7Cl3NNnKQP2mN6nlVnIM@~l= z?3W%caf*6}>2*Z?$L`b}uXrpnGFl=x3q~x5dxzFGnY8gI>#`S*On%|eML68B?YQAt&ES?H{_Dd8>$4#P4pBf=a;v3@b}aD&#MEYYmAkE zsou}8X|>mrK*Mx_KD!%gY>QfFyzmm+Zg|)ZB@;{yjT)Gp7<|BH1v`qHllH|^7bir&6%7lb zvXxyP=a=3vxsVla-FWre6a5VZ{)M@XgN<>U>sdU*7~$^0i<2!gx~HlO6YGLZ#JNM( zrDl>xF6lBhM{AQxE%t|^V;3kdmu^<^{lsLKcbr+CLD{Wu_9-PHhX7Yynk*BE3Ecy0 zTykU~J*%gHCrfvVOc4>aNsmemN&DQM1R{|V^>QgPfQh>7!p!1GoJ~OIOD6wP7=Adl zXpyKZ-P55h?_&D#O}#Vpc9~~?8QfLzNLp61+S0#2`=XI*-=Q)SqpHIveZ_)uy@?@X zF^5~`qXqvw-o4xi1=C%TyuIsOhL^Mtk8bsk^tj%g^ep9oD7{!VZ|Sk$KGqni+&pY4 z8MP!C$?%=`=X4M^kVClx0C9Z~B&u**5wZ&?OR0#X9wG&IcU=fr`p z^Q((ztp{9o#15ow&KCDH7V3q?os(_zU6HNtC&yM!J$!J`YxrVRM(53?c@q{I8AIo` zxJ*3_lpTK3tR%zE>dCeBGIBM!mMPzqQ6`S`QH~wF<+(m2v+U520dvl5TC^~=%7}tsF$Y#$_UuixNR4;Ac`Ok6eW&)U`Jcl5qtN2UJhIJ>;FqYimvOE2`y?C!edA(PK*B;tJVH7By&6Pkk<_e@%GZf!NDXt;B_ zEY;+~%XJndA=k%yo85bEO*jiBp1s4gC^%NZADORnZC`DCG&)({TfMO@yM6k>h>ska zQfl^ZTzAr3Yz|%pydtV4x9E0gx;HG3w@!*J_w1_M@S+Y<*wG{P8bz~O)_L2jT;;6= z;-8)WvVO9{a`{a?T|HgnoKp|nGCfBm25xMQmb#;t`K4QXB}#7rg}Tq+2N~w=H1$ip zvaPtv_%jbhr`XMEtp`KvubJ$M9F8ei?Ih;^q-^He_~lmLZ|nEQ%&Ajf?flNPI4QX> z=Yh7yul(|hX6c`{CvwAd^e4OzD@26rS?20E=#>agiZ#_2yA1cW_okj6Z(X~rxGA}+ zY1nRlS1FetWz!ZJ!fMPP-m++-CpEsHe&Jo+*QPx<#{&gbsnw@?>TU0{cx_9ZLVI%` z=e{A53!L@l??GUbsmtVLDH{(ckJ zloDM$A6$3*pib{yyCG@8{JPpW-$=ohffXDZde?(uzg0r6e*^6r8jZe7_#Rh8bNjS_l z7q1s(PVP46bOf6VRTd7n{W`H|-GKCHELZT8dAO#8-M*#sqDe6Pf*sU1#5TLa`D$i* zLPtiTW9d{={HBOyd2=3h3w`C3$}bw}WSQHVWL@tsX!8E?4G$SdmFheBjaz1pB-y`1 zoGpIbA!=Ke+hu0f-+XBxi*0EAlGDWEof{OTcTS7+>355bT6;_TIfrG)9p4hOT(aea zc7dFnOOcI%u)*d2-sTw{p!R1ufYU6Zuw2 zaqAY9M!$QPe4X*1k=ODkS3=dgt+)`)2!Ok*bcf&*RQA6{5H`L%T6eRg(Iz$;yB8yY z1E?Je&QMnrrT!5onY~JCrA66dcOs8)Sb*6)KGK1~VG?XzXT3aLw9hGuqjz*s;JIb=8`lEjU1 zfzv@30$mBoo?tJjMxJ{lEGMT5qHgPj{XuHuI=Q(sR=YpDd%AlNm?s2ErdH9aYjt*P zczdaij0KxVkpB;9*FB$nKV5)&oEo)kPClXned2WZ26iE?d%8#Ha3yayhNF0d2XE&_JzYEU%<p$3Kir?M`vuPJz)i= zF2@Q^V~>cvPmM?9_r9DCv>?i0r4vj~BTo?DgC9!W48w%g&175oxeM72W5?A^8}XM# zPx96`b?mUg_wT&k9fifZ%^i`3`-8ESXSuJE�Xi+0m`(i!O9PrfE8w4h7XyKypjq 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%0AT| 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 All values are in seconds. **Success rate is 100% (zero failures)** at every concurrency level. +> +> Note: as concurrency grows, ROCK applies rate limiting on the control plane to protect server-side stability, so the observed latency increases accordingly with higher concurrency. + +![Sandbox Create Latency Distribution](../../../static/img/sandbox_create_latency_distribution.png) + +## 4. Conclusion + +The benchmark from 1000 up to 16000 concurrent sandbox creations on ROCK shows: + +1. **100% success rate**, validating ROCK's reliability under heavy concurrent load. +2. **Small-scale concurrency (≤ 2000) is essentially queue-free**, with P50 below 5s — comfortably suitable for latency-sensitive training and evaluation scenarios. +3. **At large scale (≥ 4000), latency grows roughly linearly with concurrency**, giving predictable, easy-to-capacity-plan behaviour for sandbox pools. +4. **Even at 16000 concurrency, P99 stays under ~60s**, sufficient to support very large agent rollouts and parallel RL steps. + +ROCK is therefore proven capable of supporting **tens of thousands of concurrent sandbox creations**, providing solid environment infrastructure for large-scale agentic RL training. diff --git a/docs/versioned_docs/version-1.8.x/User Guides/scheduler.md b/docs/versioned_docs/version-1.8.x/User Guides/scheduler.md new file mode 100644 index 0000000000..68a63cb427 --- /dev/null +++ b/docs/versioned_docs/version-1.8.x/User Guides/scheduler.md @@ -0,0 +1,283 @@ +--- +sidebar_position: 5 +--- + +# Scheduler + +The ROCK scheduler is a periodic task framework embedded in the `admin` service. It dispatches background maintenance tasks (image cleanup, file cleanup, container cleanup, image pre-pull, custom tasks, ...) to every alive Ray worker on a configurable interval, so worker nodes stay healthy without manual intervention. + +This guide covers how to enable the scheduler, configure built-in tasks, write your own task, and inspect runtime status. + +## 1. How It Works + +- The scheduler runs inside the `admin` process as a dedicated daemon thread (`SchedulerThread`) with its own `asyncio` event loop. +- Tasks are scheduled by [APScheduler](https://apscheduler.readthedocs.io/) using fixed intervals (`interval_seconds`). +- For each tick, the scheduler resolves the list of alive Ray workers (cached via `worker_cache_ttl` seconds) and dispatches the task to every worker concurrently (default concurrency: 50). +- Dispatch is done over HTTP through the worker's **rocklet** service: the admin builds a `RemoteSandboxRuntime(host=worker_ip, port=Port.PROXY)` (see `rock.deployments.constants.Port`) and calls `runtime.execute / read_file / write_file` against it. **Every worker must therefore have the `rocklet` server running and reachable on `Port.PROXY`** — otherwise the scheduler cannot push commands or read/write status files on that worker. +- Each task subclasses `rock.admin.scheduler.task_base.BaseTask` and must implement `run_action(runtime: RemoteSandboxRuntime)` — the work performed on a single worker. +- Per-worker execution status is persisted to the worker filesystem under `ROCK_SCHEDULER_STATUS_DIR` (default `/data/scheduler_status`), and an aggregated execution report is written to `/_run_report.json` after every run. +- If a Nacos config provider is enabled, the scheduler subscribes to config changes and applies a diff: only tasks whose hash changed are re-installed; removed tasks are cleaned up from all workers. + +### Prerequisites + +Before enabling the scheduler, make sure each Ray worker meets the following requirements: + +| Requirement | Why | +|-------------|-----| +| `rocklet` process is running on the worker | The scheduler dispatches every task through the rocklet HTTP API; without it, `runtime.execute` calls time out. | +| Rocklet's listening port is reachable from the admin | The scheduler uses `Port.PROXY` (defined in `rock.deployments.constants.Port`) as the dispatch target. Make sure no firewall / security group blocks it. | +| `ROCK_SCHEDULER_STATUS_DIR` is writable inside the worker | Tasks read and write `_status.json` here for idempotency / PID tracking. | +| Tools required by the task are available on the worker | e.g. `docker` for the cleanup / pull tasks, `curl` and outbound network for `ImageCleanupTask` to install `docuum` on first run. | + +The rocklet server is started automatically by the standard worker bootstrap scripts (`docker_run.sh`, `docker_run_with_uv.sh`, `docker_run_with_pip.sh`) — typically `rocklet --port `. If you bring up workers with a custom entrypoint, ensure the equivalent command is invoked. See the [Configuration](./configuration.md) guide for the runtime-environment options that govern how rocklet is started. + +### Idempotency + +Each task declares an idempotency mode that affects how it is re-run: + +| Mode | Behavior | +|------|----------| +| `IDEMPOTENT` | Always run on every tick. Safe to repeat (e.g. `docker pull`, `find -exec rm`). | +| `NON_IDEMPOTENT` | Spawns a background daemon (e.g. `docuum`). The scheduler reads the previous status file, checks whether the recorded PID is still alive, and skips re-launch if the daemon is still running. On task removal the daemon is killed via `pkill`. | + +## 2. Enabling the Scheduler + +The scheduler is configured under the top-level `scheduler:` key of the ROCK admin YAML (e.g. `rock-conf/rock-local.yml`, `rock-conf/rock-dev.yml`). + +```yaml +scheduler: + enabled: true # Master switch + worker_cache_ttl: 43200 # Worker IP cache TTL in seconds + tasks: + # ... task list, see below +``` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `enabled` | bool | `false` | Master switch. When `false`, all tasks are removed and no new ticks fire. | +| `worker_cache_ttl` | int | `3600` | Seconds the alive-worker IP list is cached before refreshing from `ray.nodes()`. | +| `tasks` | list | `[]` | List of `TaskConfig` entries (see [Section 4](#4-task-config-schema)). | + +### Related Environment Variables + +| Variable | Default | Purpose | +|----------|---------|---------| +| `ROCK_SCHEDULER_STATUS_DIR` | `/data/scheduler_status` | Directory on workers where per-task status JSON and run reports are written. | +| `ROCK_LOGGING_PATH` | (unset) | When set, scheduler-spawned daemons (docuum, container_cleanup, image_pull) redirect their stdout/stderr to `/.log`. | +| `ROCK_DOCUUM_INSTALL_URL` | `https://raw.githubusercontent.com/stepchowfun/docuum/main/install.sh` | Install script URL for `docuum`, fetched on demand by `ImageCleanupTask`. | + +## 3. Built-in Tasks + +ROCK ships with four built-in tasks under `rock.admin.scheduler.tasks`. Each task is registered by setting `task_class` to its fully qualified class path. + +### 3.1 ImageCleanupTask + +Runs [`docuum`](https://github.com/stepchowfun/docuum) on every worker to evict the least-recently-used Docker images once disk usage crosses a threshold. **Non-idempotent** — `docuum` runs as a long-lived daemon; the scheduler tracks its PID and skips re-launch while the daemon is alive. + +```yaml +- task_class: rock.admin.scheduler.tasks.image_cleanup_task.ImageCleanupTask + enabled: true + interval_seconds: 43200 # Re-check daemon every 12 hours + params: + disk_threshold: "70%" # Trigger eviction when disk usage exceeds 70% + image_whitelist: # Glob patterns matching repository:tag — never evicted + - "python:3.11" + - "my-registry.example.com/base/*" +``` + +| Param | Type | Default | Description | +|-------|------|---------|-------------| +| `disk_threshold` | str | `"1T"` | Disk usage threshold passed to `docuum --threshold`. Accepts size (`100G`, `1T`) or percentage (`70%`). | +| `image_whitelist` | list[str] | `[]` | Glob patterns forwarded to `docuum --keep`. | + +### 3.2 FileCleanupTask + +Walks each configured directory and removes files that are either older than `max_age_mins` or larger than `max_file_size`, then prunes empty subdirectories. **Idempotent**. + +```yaml +- task_class: rock.admin.scheduler.tasks.file_cleanup_task.FileCleanupTask + enabled: true + interval_seconds: 86400 # Run daily + params: + target_dirs: + # Plain string form — no exclusions + - "/data/service_status" + # Object form — per-directory exclusions + - path: "/data/logs" + exclude_files: # Plain name | relative path | absolute path + - "docuum.log" + - "./rocklet.log" + - "./access.log" + exclude_dirs: + - ".cache" + max_age_mins: 10080 # 7 days; older files are removed + max_file_size: "1G" # Files larger than this are removed (supports K/M/G/T) +``` + +| Param | Type | Default | Description | +|-------|------|---------|-------------| +| `target_dirs` | list | `[]` | Each entry is either a string (path only) or `{path, exclude_files, exclude_dirs}`. | +| `max_age_mins` | int | `10080` | Files whose mtime is older than this many minutes are deleted. | +| `max_file_size` | str | `"1G"` | Files larger than this are deleted. Suffixes `K/M/G/T` accepted. | + +The deletion condition is `(-mmin +max_age_mins) OR (-size +max_file_size)`. After file removal, a second `find -depth -type d -empty -delete` pass removes empty directories left behind (also honoring `exclude_dirs`). + +### 3.3 ContainerCleanupTask + +Removes stopped Docker containers older than a configurable age. Helps prevent the worker's container list from growing unbounded between sandbox runs. **Idempotent**. + +```yaml +- task_class: rock.admin.scheduler.tasks.container_cleanup_task.ContainerCleanupTask + enabled: true + interval_seconds: 86400 + params: + max_age_hours: 72 # Remove exited containers older than 72 hours +``` + +| Param | Type | Default | Description | +|-------|------|---------|-------------| +| `max_age_hours` | int | `24` | Maximum age (hours since `FinishedAt`) for kept exited containers. Older ones are `docker rm`'d. | + +The task also removes any container in the `created` state (never started) on every run. + +### 3.4 ImagePullTask + +Pre-pulls a list of Docker images on every worker, optionally logging in to private registries first. Reduces sandbox cold-start latency. **Idempotent** (`docker pull` is a no-op when the image is already up-to-date). + +```yaml +- task_class: rock.admin.scheduler.tasks.image_pull_task.ImagePullTask + enabled: true + interval_seconds: 21600 # Refresh every 6 hours + params: + images: + # Plain string form — public image, no auth + - "python:3.11" + # Object form — private image with registry login + - image: "my-registry.example.com/chatos/python:313" + registry_username: "myuser" + registry_password: "bXlwYXNzd29yZA==" # base64-encoded +``` + +| Param | Type | Default | Description | +|-------|------|---------|-------------| +| `images` | list | `[]` | Each entry is either an image string or `{image, registry_username, registry_password}`. | + +`registry_password` must be base64-encoded; the worker decodes it and pipes it to `docker login --password-stdin`. The registry host is parsed from the image name, so each image can target a different registry. + +## 4. Task Config Schema + +Every entry under `scheduler.tasks` is loaded as a `rock.config.TaskConfig`: + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `task_class` | str | `""` | Fully qualified Python class path. Required. | +| `enabled` | bool | `true` | Disabled tasks are skipped at install time and torn down on reload. | +| `interval_seconds` | int | `3600` | APScheduler `interval` in seconds. | +| `params` | dict | `{}` | Task-specific kwargs forwarded to `from_config()`. | + +A change in any field of an existing task entry causes the scheduler to uninstall the old task (cleaning up its worker-side state when non-idempotent) and install the new one — without restarting the admin process. + +## 5. Writing a Custom Task + +Any class under your Python path that subclasses `BaseTask` can be registered. The minimum contract is: + +```python +# my_pkg/my_tasks/disk_report_task.py +from rock.admin.proto.request import SandboxCommand as Command +from rock.admin.scheduler.task_base import BaseTask, IdempotencyType, TaskStatusEnum +from rock.sandbox.remote_sandbox import RemoteSandboxRuntime + + +class DiskReportTask(BaseTask): + """Log `df -h` output from every worker.""" + + def __init__(self, interval_seconds: int = 3600, mount_point: str = "/"): + super().__init__( + type="disk_report", # Used as job id and status filename prefix + interval_seconds=interval_seconds, + idempotency=IdempotencyType.IDEMPOTENT, + ) + self.mount_point = mount_point + + @classmethod + def from_config(cls, task_config) -> "DiskReportTask": + return cls( + interval_seconds=task_config.interval_seconds, + mount_point=task_config.params.get("mount_point", "/"), + ) + + async def run_action(self, runtime: RemoteSandboxRuntime) -> dict: + result = await runtime.execute( + Command(command=f"df -h {self.mount_point}", shell=True), + ) + return { + "status": TaskStatusEnum.SUCCESS, + "exit_code": result.exit_code, + "stdout": result.stdout, + } +``` + +Then register it from YAML: + +```yaml +scheduler: + enabled: true + tasks: + - task_class: my_pkg.my_tasks.disk_report_task.DiskReportTask + enabled: true + interval_seconds: 600 + params: + mount_point: "/data" +``` + +### Authoring Checklist + +- **Always set a unique `type` string** in `super().__init__()`. It is used as the APScheduler job id, the status filename (`_status.json`), and the run-report filename (`_run_report.json`). Two tasks must not share a `type`. +- **Pick the right `IdempotencyType`**: + - Use `IDEMPOTENT` when `run_action` finishes synchronously and is safe to re-execute. + - Use `NON_IDEMPOTENT` when you `nohup` a long-running daemon and return its PID. The scheduler will then track the PID, skip re-launch while it is alive, and `pkill` it on uninstall. +- **Return a dict from `run_action`**. Recommended keys: + - `status` — a `TaskStatusEnum` value, persisted into the status file. + - `pid` — required for `NON_IDEMPOTENT` daemons (use `rock.utils.system.extract_nohup_pid` on the `nohup ... & echo PID_PREFIX${{!}}PID_SUFFIX` output). + - Any other diagnostic fields are written to the status file's `extra` section. +- **Override `from_config(cls, task_config)`** to translate `task_config.params` into your `__init__` kwargs. +- **Use `runtime.execute / read_file / write_file`** rather than running shell commands locally — the scheduler is dispatching to remote workers via `RemoteSandboxRuntime`. + +## 6. Observability + +For each task, the scheduler writes two artifacts on every worker: + +| Path | Written By | Contents | +|------|------------|----------| +| `/_status.json` | `BaseTask.save_task_status` | Latest per-worker status: `task_name`, `worker_ip`, `pid`, `status` (`pending`/`running`/`success`/`failed`), `last_run`, `error`, plus task-specific `extra` fields. | +| `/_run_report.json` | `BaseTask.run` (admin side, after the tick completes) | Aggregated report: total/success/failed counts, list of `success_ips`, and `failed_details` (`ip` + traceback). | + +Scheduler-internal logs are written under the standard ROCK admin log path, with `name="scheduler"`, `name="task_base"`, `name="image_clean"`, etc. Scheduler-spawned daemons additionally write their own logs to `/.log` when `ROCK_LOGGING_PATH` is set (e.g. `docuum.log`, `container_cleanup.log`, `image_pull.log`). + +## 7. Dynamic Reload via Nacos (Optional) + +When the admin service is configured with a Nacos provider, the scheduler installs a YAML listener and reacts to config pushes: + +- Only the `scheduler:` section is inspected; other sections are ignored. +- The new section is hashed and compared against the previous one — duplicate notifications are skipped. +- A diff between old and new task lists determines which tasks to install, uninstall, or reinstall (changed `params` / `interval_seconds` / `enabled`). +- Non-idempotent tasks that are removed or re-installed are first cleaned up: their daemon PID is killed and the status file is removed. + +This means task interval changes, parameter tweaks, and adding/removing tasks can be applied without restarting the admin process. + +## 8. Troubleshooting + +| Symptom | Likely Cause | What to Check | +|---------|--------------|----------------| +| `Scheduler disabled, all tasks removed` in admin log | `scheduler.enabled` is `false` | Set `enabled: true` in YAML. | +| `No alive workers found for task ''` | Ray cluster has no live worker nodes | Verify `ray.nodes()` reports alive CPU workers; consider lowering `worker_cache_ttl` if workers were just added. | +| Task ticks fire but every worker shows up in `failed_details` with connection errors | `rocklet` is not running on the workers, or `Port.PROXY` is blocked | On the worker host, hit the rocklet liveness endpoint `GET /is_alive` on `Port.PROXY` (e.g. `curl http://:/is_alive`); if it does not respond, restart rocklet (`rocklet --port `) or open the port in the firewall. | +| Task runs but never repeats on a non-idempotent worker | Recorded PID still alive | Inspect `/_status.json`; if `status: running` and the PID is alive, `should_run` returns `False`. | +| `Failed to create task ''` | `task_class` import failed | Ensure the module is importable inside the admin process (installed in the same venv, on `PYTHONPATH`). | +| `docker login` failing in `ImagePullTask` | `registry_password` not base64-encoded, or wrong registry parsed from image | Re-encode the password with `echo -n '' \| base64`; double-check the image's registry host. | + +## Related Documents + +- [Configuration](./configuration.md) — Environment variables and runtime layout +- [API Documentation](../References/api.md) — Admin HTTP API +- [Python SDK Documentation](../References/Python%20SDK%20References/python_sdk.md) — Programmatic sandbox usage