From 9b4ad9fda5fd8f2a3f0dfbfc9688be38ce649ccc Mon Sep 17 00:00:00 2001
From: Praveer Rai <9212232+praveer-rai@users.noreply.github.com>
Date: Fri, 17 Apr 2026 21:03:45 +0200
Subject: [PATCH 1/2] docs: rewrite README with clearer motivation and
before/after examples
---
README.md | 80 ++++++++++++++++++++++++++++++++++++++++++++-----------
1 file changed, 64 insertions(+), 16 deletions(-)
diff --git a/README.md b/README.md
index de666f3..cdf00d2 100644
--- a/README.md
+++ b/README.md
@@ -6,10 +6,16 @@
Tame your async — no platform threads were harmed.
-A tiny Java 21 library that lets you wait for async results on virtual threads,
-so your platform threads stay free and your context tags along for the ride.
+A tiny Java 21 library for virtual thread concurrency. Two things:
-## Why
+1. **`Blockless.get()`** — wait for async results safely, without entering the future's internals
+2. **`Parallel`** — fan out work on virtual threads, collect results, keep your trace context
+
+So your platform threads stay free and your context tags along for the ride.
+
+Zero dependencies in core. Pluggable context propagation for MDC, gRPC, and OpenTelemetry.
+
+## Why `Blockless.get()`
`CompletableFuture.join()` parks your thread inside the future's own internals.
If that implementation uses `synchronized` or you're on a platform thread, you
@@ -27,19 +33,42 @@ String result = someFuture.join();
String result = Blockless.get(someFuture);
```
-## Quick start
+Also works with callables — runs them on a virtual thread and blocks for the result:
```java
-// Wait for a CompletionStage without blocking platform threads
-var result = Blockless.get(CompletableFuture.supplyAsync(() -> "hello"));
-
-// Run a Callable on a virtual thread and get the result
var answer = Blockless.get(() -> expensiveComputation());
```
-## Parallel execution
+## Why `Parallel`
+
+Services often need to fan out N calls and collect the results. The typical
+pattern looks like this:
+
+```java
+// Before: manual executor + supplyAsync + semaphore + join
+var executor = Executors.newVirtualThreadPerTaskExecutor();
+var semaphore = new Semaphore(10);
+var futures = ids.stream()
+ .map(id -> CompletableFuture.supplyAsync(() -> {
+ semaphore.acquireUninterruptibly();
+ try { return fetchById(id); }
+ finally { semaphore.release(); }
+ }, executor))
+ .toList();
+var results = futures.stream().map(CompletableFuture::join).toList();
+```
-Run work concurrently on virtual threads with context propagation built in:
+With blockless:
+
+```java
+// After: one line, same behavior
+var results = parallel.withMaxConcurrency(10).map(ids, this::fetchById);
+```
+
+Each task runs on its own virtual thread. Results stay in input order.
+MDC and trace context survive the hop.
+
+### Usage
```java
var parallel = Parallel.create(new Slf4jMdcContextPropagator());
@@ -47,27 +76,43 @@ var parallel = Parallel.create(new Slf4jMdcContextPropagator());
// Map in parallel, results stay in order
List names = parallel.map(userIds, id -> fetchName(id));
-// Fire off an async task
+// Fire off an async task, get result later
Supplier data = parallel.async(() -> fetchData());
// Build a map in parallel
Map profiles = parallel.asMap(userIds, id -> loadProfile(id));
-// Limit to 10 concurrent tasks (extras park on virtual threads until a permit frees up)
+// Limit concurrent tasks (extras park until a permit frees up)
var bounded = parallel.withMaxConcurrency(10);
+List names = bounded.map(userIds, id -> fetchName(id));
+
+// Collect results without failing fast — failed tasks return Either.fail()
+List> results = parallel.toEither(ids, id -> riskyFetch(id));
```
## Context propagation
-Thread-local context (MDC, gRPC context, OpenTelemetry spans) doesn't survive the
-hop to a new thread. Blockless fixes that.
+When you hop to a new thread, thread-local context (MDC trace IDs, gRPC context,
+OpenTelemetry spans) is lost. Logs become uncorrelated. Traces break.
+
+`Parallel` handles this automatically — context is captured when you call `map()`/`async()`
+and restored in each worker thread. You choose which propagators to wire up:
```java
-// Wrap a Callable — MDC comes along, get a result back
+var parallel = Parallel.create(
+ new Slf4jMdcContextPropagator(),
+ new GrpcContextPropagator()
+);
+```
+
+For lower-level use, you can wrap individual tasks:
+
+```java
+// Wrap a Callable — MDC comes along
Callable wrapped = CallableContext.wrap(task, new Slf4jMdcContextPropagator());
// Wrap a Runnable
-Runnable wrappedRunnable = RunnableContext.wrap(task, new Slf4jMdcContextPropagator());
+Runnable wrapped = RunnableContext.wrap(task, new Slf4jMdcContextPropagator());
// Wrap an entire ExecutorService
ExecutorService executor = PropagatingExecutorService.wrap(
@@ -85,6 +130,9 @@ ExecutorService executor = PropagatingExecutorService.wrap(
| `blockless-context-grpc` | gRPC `Context` |
| `blockless-context-opentelemetry` | OpenTelemetry `Context` |
+Framework dependencies are `provided` scope — blockless uses whatever version
+your service already has. No version conflicts.
+
## Modules
| Module | What it does |
From 5cb981f014b01d38004573a22f0faf259731c138 Mon Sep 17 00:00:00 2001
From: Praveer Rai <9212232+praveer-rai@users.noreply.github.com>
Date: Fri, 17 Apr 2026 21:06:12 +0200
Subject: [PATCH 2/2] docs: add back Quick start section
---
README.md | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/README.md b/README.md
index cdf00d2..7aaef58 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,16 @@ So your platform threads stay free and your context tags along for the ride.
Zero dependencies in core. Pluggable context propagation for MDC, gRPC, and OpenTelemetry.
+## Quick start
+
+```java
+// Wait for a CompletionStage without blocking platform threads
+var result = Blockless.get(CompletableFuture.supplyAsync(() -> "hello"));
+
+// Run a Callable on a virtual thread and get the result
+var answer = Blockless.get(() -> expensiveComputation());
+```
+
## Why `Blockless.get()`
`CompletableFuture.join()` parks your thread inside the future's own internals.