Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,32 @@ jobs:
target/*.jar
target/site/jacoco/**
if-no-files-found: error

auxiliary-modules:
name: Verify integrations, examples, and benchmarks
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Set up Temurin JDK 21
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "21"
cache: maven

- name: Install current core
run: mvn -B -ntp clean install

- name: Test SLF4J integration
run: mvn -B -ntp -f integrations/threadforge-slf4j/pom.xml clean verify

- name: Test Micrometer integration
run: mvn -B -ntp -f integrations/threadforge-micrometer/pom.xml clean verify

- name: Compile examples
run: mvn -B -ntp -f examples/pom.xml clean package

- name: Compile benchmarks
run: mvn -B -ntp -f benchmarks/pom.xml clean package -DskipTests
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ ScheduledTask scheduleAtFixedRate(Duration initial, Duration period, Runnable ru
ScheduledTask scheduleWithFixedDelay(Duration initial, Duration delay, Runnable runnable)
```

`DelayScheduler.singleThread()` 拥有底层线程并实现 `AutoCloseable`,应使用 try-with-resources;`shared()` 和 `from(executor)` 的 `close()` 不会关闭共享或外部执行器。

优先级调度器:

```java
Expand All @@ -257,6 +259,8 @@ Scheduler.priority(int size)
scope.defer(() -> resource.close());
```

`close()` 会等待已启动工作真正退出后再执行 deferred cleanup。忽略中断的用户代码会让关闭继续等待,直到代码自行结束。

### Task

```java
Expand All @@ -275,6 +279,8 @@ CompletableFuture<T> toCompletableFuture()
CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)
```

`toCompletableFuture()` 返回只读结果镜像;组合操作保持兼容,但外部完成或取消镜像不会修改底层 `Task`。

### FailurePolicy

- `FAIL_FAST`:首个失败直接抛出,并取消其他任务
Expand Down
13 changes: 12 additions & 1 deletion docs/ai/threadforge-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,25 @@ try (ThreadScope scope = ThreadScope.open()) {
- `ThreadScope.open().withFailurePolicy().withDeadline().withScheduler().withRetryPolicy().withConcurrencyLimit()`
- `scope.submit(name, callable)` — submit a value-returning task
- `scope.submit(name, runnable)` — submit a basic no-result task and receive `Task<Void>`
- `task.toCompletableFuture()` — observe/compose results only; use `task.cancel()` for cancellation
- `Channel.send/receive` — blocking waits are interruptible and throw `CancelledException`
- `task.await()` / `scope.await(...)` — caller interruption propagates without changing target task states
- `scope.close()` — waits for started work to exit before deferred cleanup; interruption-ignoring code keeps it blocked
- Registrations racing with `close()` either succeed and are cleaned up or fail with `IllegalStateException`
- `scope.await(tasks)` / `scope.awaitAll(tasks)` — wait for completion
- `scope.joiner().firstSuccess(...)` — return first successful result, cancel unfinished siblings
- `scope.joiner().quorum(n, ...)` — return once `n` tasks succeed
- `scope.joiner().hedged(delay, primary, backup...)` — start one task now and release backup tasks after the hedge delay
- `SlowTaskHook.create(threshold, consumer)` — emit events for tasks slower than a threshold
- ThreadLocal hooks install and restore context on the same runner thread, including timeout/cancel paths
- `hookA.andThen(hookB)` — compose multiple hooks
- `task.await()` — get single task result
- `scope.schedule(duration, callable)` — delayed execution
- `scope.scheduleAtFixedRate(initial, period, runnable)` — periodic execution

## FailurePolicy

- `FAIL_FAST` (default) — first failure cancels all, throws
- `FAIL_FAST` (default) — first completed failure cancels all, throws
- `SUPERVISOR` — no auto-cancel, check `Outcome.hasFailures()`
- `COLLECT_ALL` — wait all, throw `AggregateException`
- `CANCEL_OTHERS` — cancel siblings, don't throw
Expand All @@ -42,6 +48,7 @@ try (ThreadScope scope = ThreadScope.open()) {
- Always use try-with-resources for `ThreadScope`
- Default deadline is 30 seconds — override with `.withDeadline()`
- `RetryPolicy.maxAttempts` includes the first attempt (3 = 1 initial + 2 retries)
- Exponential-backoff multipliers must be finite and at least `1.0`; delays clamp safely to `maxDelay`
- `Context` auto-propagates from submit thread to task thread
- `ScopeJoiner` launches tasks inside the same `ThreadScope`; deadline, cancellation, retry, and hooks still apply
- Basic `Runnable` submissions use scope defaults; use `Callable<T>` overloads for per-task priority, retry, or timeout overrides
Expand All @@ -52,6 +59,10 @@ try (ThreadScope scope = ThreadScope.open()) {
- `Scheduler.detect()` — auto-selects virtual threads (JDK 21+) or common pool
- `Scheduler.fixed(n)` — fixed thread pool
- `Scheduler.priority(n)` — priority-based pool (use with `TaskPriority`)
- Owned schedulers (`fixed`/`priority`) belong to one scope and close with it; use `Scheduler.from(...)` for a caller-managed executor shared across scopes
- Submissions to a shut-down scheduler are rejected instead of remaining pending
- `DelayScheduler.singleThread()` is `AutoCloseable` and owned; closing `shared()` or `from(executor)` does not close shared/external executors
- One-shot/initial delays may be zero but not negative; fixed-rate periods and fixed delays must be positive and retain nanosecond precision

## Exceptions

Expand Down
18 changes: 17 additions & 1 deletion docs/ai/threadforge.SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ Joiner rules:

| Policy | Behavior | Use Case |
|---|---|---|
| `FAIL_FAST` | First failure throws, cancel remaining | Default; fail-fast pipelines |
| `FAIL_FAST` | First completed failure throws, cancel remaining | Default; fail-fast pipelines |
| `COLLECT_ALL` | Wait all, throw `AggregateException` if any failed | Batch jobs needing all results |
| `SUPERVISOR` | Never auto-cancel; failures in `Outcome` | Independent tasks, no cascading |
| `CANCEL_OTHERS` | Cancel siblings on failure, don't throw | Fan-out with graceful degradation |
Expand All @@ -192,6 +192,7 @@ RetryPolicy.builder() // Full customiza
```

Note: `maxAttempts` includes the first attempt. CancelledException and Error are never retried by default.
Exponential-backoff multipliers must be finite and at least `1.0`; computed delays are clamped to `maxDelay` without numeric overflow.

### Scheduler

Expand All @@ -204,6 +205,8 @@ Scheduler.virtualThreads() // Explicit virtual threads, scope does NOT own
Scheduler.from(executorService) // Wrap external executor, scope does NOT own
```

An owned scheduler (`fixed`/`priority`) belongs to one scope and is closed with it. Use `from(...)` with a caller-managed executor when multiple scopes must share an executor. Submissions after shutdown are rejected.

### Context Propagation

ThreadForge automatically captures `Context` at submit time and restores it in the task thread. After task completes, original thread context is restored.
Expand Down Expand Up @@ -236,6 +239,8 @@ channel.close(); // Signal no more sends
// Iterable: for (T v : channel) { ... } stops after closed and drained
```

Blocked `send`/`receive` calls are interruptible and surface cancellation as `CancelledException` while preserving the interrupt flag.

### Scheduling

```java
Expand All @@ -254,6 +259,9 @@ ScheduledTask poll = scope.scheduleWithFixedDelay(
t.cancel();
```

For direct scheduling, `DelayScheduler.singleThread()` is owned and must be closed (prefer try-with-resources). Closing `DelayScheduler.shared()` or `DelayScheduler.from(executor)` does not close the shared or external executor.
One-shot delays and periodic initial delays may be zero but not negative; fixed-rate periods and fixed delays must be positive. Scheduling preserves nanosecond precision.

### Task Composition

```java
Expand Down Expand Up @@ -420,6 +428,8 @@ try (ThreadScope scope = ThreadScope.open()
// Requires OpenTelemetry API on classpath
```

ThreadLocal-style hooks are installed and restored on the same runner thread. A shared hook must distinguish tasks by both scope ID and task ID; timeout signals do not move context cleanup to the timer thread.

## Common Mistakes

| Mistake | Fix |
Expand All @@ -441,7 +451,13 @@ try (ThreadScope scope = ThreadScope.open()
| `ExecutorService.submit()` | `scope.submit()` |
| `CompletableFuture.get()` | `task.await()` |
| `CompletableFuture.allOf()` | `scope.await(tasks)` / `scope.awaitAll(tasks)` |

| `ExecutorService.shutdownNow()` | `scope.close()` (try-with-resources) |
| `ThreadLocal` manual propagation | `Context.put/get` (auto-propagated) |
| Custom retry loops | `RetryPolicy` + `scope.withRetryPolicy()` |
| Manual timeout management | `scope.withDeadline()` or per-task `Duration` timeout |

`task.toCompletableFuture()` is an observation-only mirror. Do not use it to complete or cancel the underlying task; call `task.cancel()` for framework-aware cancellation.
Interrupting a thread blocked in `task.await()` or `scope.await(...)` throws `CancelledException` without changing target task states.
`scope.close()` waits for started work to physically exit before deferred cleanup; code that ignores interruption keeps close blocked.
Registrations racing with close either succeed and are cleaned up, or fail with `IllegalStateException`; no resource is accepted after close.
14 changes: 13 additions & 1 deletion docs/ai/threadforge.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ try (ThreadScope scope = ThreadScope.open()

| Policy | Behavior |
|---|---|
| `FAIL_FAST` | First failure throws, cancel remaining |
| `FAIL_FAST` | First completed failure throws, cancel remaining |
| `COLLECT_ALL` | Wait all, throw `AggregateException` if any failed |
| `SUPERVISOR` | Never auto-cancel; failures in `Outcome` |
| `CANCEL_OTHERS` | Cancel siblings on failure, don't throw |
Expand All @@ -55,6 +55,11 @@ Scheduler.virtualThreads() // Explicit virtual threads
Scheduler.from(executorService) // Wrap external executor
```

An owned scheduler (`fixed`/`priority`) belongs to one scope and is closed with it. Use `from(...)` with a caller-managed executor when multiple scopes must share an executor. Submissions after shutdown are rejected.

`DelayScheduler.singleThread()` is `AutoCloseable` and owns its executor. Closing `DelayScheduler.shared()` or `DelayScheduler.from(executor)` does not close the shared or external executor.
One-shot delays and periodic initial delays may be zero but not negative; fixed-rate periods and fixed delays must be positive. Scheduling preserves nanosecond precision.

## RetryPolicy

```java
Expand All @@ -64,6 +69,7 @@ RetryPolicy.exponentialBackoff(3, initial, 2.0, maxDelay) // Exponential backof
```

Note: `maxAttempts` includes the first attempt (3 = 1 initial + 2 retries).
Exponential-backoff multipliers must be finite and at least `1.0`; computed delays are clamped to `maxDelay` without numeric overflow.

## ScopeJoiner

Expand Down Expand Up @@ -91,6 +97,7 @@ ThreadHook combined = SlowTaskHook.create(Duration.ofMillis(200), event -> {
- `ThreadHook.andThen(...)` composes multiple hooks
- `SlowTaskHook` emits events only when duration crosses the configured threshold
- optional Micrometer and SLF4J / MDC integrations live under `integrations/`
- ThreadLocal hook setup and cleanup run on the same task runner; hook state is scoped by scope ID plus task ID

## Context Propagation

Expand Down Expand Up @@ -126,6 +133,11 @@ String id = Context.get("traceId"); // "req-1001"
- `ExecutorService.submit()` → `scope.submit()`
- `CompletableFuture.get()` → `task.await()`
- `CompletableFuture.allOf()` → `scope.awaitAll(tasks)`
- `task.toCompletableFuture()` is observation-only; use `task.cancel()` to cancel the task
- Blocking `Channel.send/receive` calls are interruptible and throw `CancelledException`
- Interrupting an `await` caller does not mark target tasks cancelled
- `scope.close()` waits for started work to exit before running deferred cleanup
- `submit`, `schedule*`, and `defer` racing with close either register fully or fail clearly
- `shutdownNow()` → `scope.close()` (try-with-resources)
- Manual retry loops → `RetryPolicy`
- Manual timeout → `withDeadline()` or per-task `Duration`
2 changes: 1 addition & 1 deletion docs/api/control/FailurePolicy.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

### `FAIL_FAST`

- 行为:首个失败立即抛出,取消其余任务
- 行为:按实际完成顺序观察任务;首个失败立即抛出并取消其余任务
- 适用:强一致聚合、任意子任务失败即整体失败

### `COLLECT_ALL`
Expand Down
2 changes: 2 additions & 0 deletions docs/api/control/RetryPolicy.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
### `RetryPolicy.exponentialBackoff(int maxAttempts, Duration initialDelay, double multiplier, Duration maxDelay)`

- 指数退避重试,延迟计算为 `initialDelay * multiplier^(attempt-1)`
- `multiplier` 必须是有限数且大于等于 `1.0`
- 延迟不会超过 `maxDelay`
- 超大 Duration 或计算溢出时仍稳定截断到 `maxDelay`,不会产生负延迟

## 在 ThreadScope 中使用

Expand Down
5 changes: 3 additions & 2 deletions docs/api/core/Task.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,16 @@

### `CompletableFuture<T> toCompletableFuture()`

暴露底层 `CompletableFuture`,便于与生态 API 互操作。
返回只读的结果镜像,便于与生态 API 互操作。任务的成功、失败和取消会传播到镜像;
对镜像调用 `complete`、`completeExceptionally` 或 `cancel` 不会修改底层任务。

## 取消

### `boolean cancel()`

请求取消任务。

- 会将状态设为 `CANCELLED`
- 仅在赢得终态竞争时将状态设为 `CANCELLED`
- 若任务正在运行,会中断运行线程
- 返回值语义与 `CompletableFuture.cancel(true)` 一致

Expand Down
10 changes: 7 additions & 3 deletions docs/api/core/ThreadScope.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ try (ThreadScope scope = ThreadScope.open()
- `deadline = Duration.ofSeconds(30)`
- 自动传播 `Context`(提交/调度时捕获,执行时恢复)
- 作用域关闭时自动取消未完成任务和计划任务
- `submit`、`schedule*`、`defer` 与 `close` 原子竞争:注册成功必由关闭流程处理,关闭先发生则注册抛 `IllegalStateException`

## API 清单

Expand Down Expand Up @@ -257,6 +258,7 @@ try (ThreadScope scope = ThreadScope.open()
- `TaskTimeoutException`:任务级超时
- `RuntimeException`:`FAIL_FAST` 下的首个失败
- `AggregateException`:`COLLECT_ALL` 下有失败
- 等待线程被中断时保留中断标记并立即抛 `CancelledException`,不会修改目标任务状态

### `Outcome await(Task<?> first, Task<?>... rest)`

Expand Down Expand Up @@ -306,13 +308,15 @@ try (ThreadScope scope = ThreadScope.open()
1. 触发 token 取消
2. 取消全部计划任务
3. 取消全部未完成任务
4. 依次执行 `defer` 清理(LIFO)
5. 取消截止时间监控任务
6. 关闭由 scope 持有的执行器
4. 等待已启动的任务和计划工作真正退出
5. 依次执行 `defer` 清理(LIFO)
6. 取消截止时间监控任务
7. 关闭由 scope 持有的执行器

异常语义:

- `close()` 本身是幂等的
- 用户代码若忽略中断,`close()` 会继续等待,直到该代码自行退出;框架不会把逻辑取消伪装成物理结束
- 清理过程中的异常会聚合(通过 `addSuppressed`)后抛出
- 如果主流程已有异常(比如 try-with-resources 体内抛错),清理异常会以 suppressed 形式附加

Expand Down
2 changes: 2 additions & 0 deletions docs/api/dataflow/Channel.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@
发送一个元素。

- 缓冲满时阻塞
- 等待线程被中断时保留中断标记并抛 `CancelledException`
- 通道关闭后抛 `ChannelClosedException`

### `T receive()`

接收一个元素。

- 缓冲空且未关闭时阻塞
- 等待线程被中断时保留中断标记并抛 `CancelledException`
- 通道已关闭且已耗尽时抛 `ChannelClosedException`

### `void close()`
Expand Down
2 changes: 2 additions & 0 deletions docs/api/observability/OpenTelemetryHook.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,6 @@ try (ThreadScope scope = ThreadScope.open()

- ThreadForge 在 `submit/schedule` 时会捕获当前 OpenTelemetry 上下文
- 任务执行前恢复父上下文,再创建任务 span
- Span Scope 在创建它的 runner 线程中关闭并恢复 ThreadLocal,上下文恢复后再结束 Span
- timeout 等终态信号不会把 Scope 的关闭转移到控制调度线程
- 该机制同时适用于平台线程和虚拟线程
22 changes: 20 additions & 2 deletions docs/api/runtime/DelayScheduler.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@

`DelayScheduler` 提供延迟和周期任务调度能力。

- 类型:`public final class DelayScheduler`
- 类型:`public final class DelayScheduler implements AutoCloseable`
- 线程安全:可在多个 scope 间共享

## 工厂方法

### `static DelayScheduler singleThread()`

创建单线程延迟调度器
创建拥有底层执行器的单线程延迟调度器。使用后应调用 `close()`,推荐 try-with-resources

### `static DelayScheduler shared()`

Expand All @@ -27,6 +27,21 @@
- 参数:`executor != null`
- 生命周期:外部执行器由调用方负责关闭

## 生命周期

### `void close()`

- `singleThread()`:关闭 owned executor,终止其线程;关闭后提交新任务会被拒绝
- `shared()`:不关闭框架全局共享 executor
- `from(executor)`:不关闭外部 executor
- 重复调用是幂等的

```java
try (DelayScheduler scheduler = DelayScheduler.singleThread()) {
scheduler.schedule(Duration.ofMillis(10), () -> runOnce());
}
```

## 调度方法

### `<T> ScheduledTask schedule(Duration delay, Callable<T> callable)`
Expand All @@ -48,6 +63,9 @@
通用约束:

- 参数不可为 `null`
- 一次性 `delay` 和周期任务 `initial` 必须大于等于 0
- fixed-rate `period` 和 fixed-delay `delay` 必须大于 0
- 使用纳秒精度;超出 `long` 纳秒范围的正 Duration 按 `Long.MAX_VALUE` 纳秒处理
- 返回值均为 `ScheduledTask`,可取消

## 与 ThreadScope 的关系
Expand Down
9 changes: 7 additions & 2 deletions docs/api/runtime/Scheduler.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
- 线程池特性:
- 核心线程数 = 最大线程数 = `size`
- 队列容量 = `max(256, size * 100)`
- 拒绝策略 = `CallerRunsPolicy`
- 队列满且执行器仍运行时,由提交线程执行
- 执行器已关闭时,明确抛出 `RejectedExecutionException`
- 允许核心线程超时回收

### `static Scheduler priority(int size)`
Expand All @@ -35,7 +36,7 @@
- 线程池特性:
- 核心线程数 = 最大线程数 = `size`
- 队列类型 = `PriorityBlockingQueue`
- 拒绝策略 = `CallerRunsPolicy`
- 执行器已关闭时,明确抛出 `RejectedExecutionException`
- 说明:
- 需配合 `TaskPriority` 使用
- 同优先级按提交顺序执行
Expand Down Expand Up @@ -75,6 +76,10 @@

是否虚拟线程模式。

## 生命周期

`fixed(...)` 和 `priority(...)` 返回 owned scheduler,应只绑定一个 `ThreadScope`;scope 关闭时会关闭其执行器,之后再次提交会被明确拒绝。需要跨 scope 复用时,请用 `from(executor)` 包装由调用方管理的外部执行器。

## 推荐用法

```java
Expand Down
Loading
Loading