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
80 changes: 75 additions & 5 deletions examples/jobs-java/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,81 @@ The registry forwards the cancel to the owner replica via
controller stack), which fires the in-process cancel token and aborts
the in-flight job.

## What's NOT in Phase B
## Phase 2: Event injection (v2.2)

- Idempotency keys for retries (Phase 3).
- Resumable checkpoints (Phase 3).
- Webhook/SSE notifications (Phase 3).
- SEP-1686 wire surfacing (Phase 2 — strictly additive on top).
The event-channel extension added in v2.2 lets a running `task=true`
handler drain a per-job event log inline, and lets any caller
holding the `jobId` post events into that same log. Two more agents
demonstrate the pattern end-to-end:

- `event-aware-provider-java/` — registers `event_aware_long_task`
as `task=true`. Loops on
`controller.recvEvent(List.of("work", "stop"), Duration.ofSeconds(30))`,
processing `work` events and exiting cleanly on `stop`.
- `event-aware-consumer-java/` — registers `drive_event_aware_task`
which depends on `event_aware_long_task`. Submits the job, walks an
`EventSubscription` on a daemon thread, posts 3 `work` events + 1
`stop` via `MeshJobs.postEvent`, then awaits the terminal result.

Java has no async/await, so the subscriber runs on a separate daemon
thread joined back on the main thread with a bounded timeout. The
`EventSubscription` iterator is wrapped in try-with-resources so its
"keep polling" flag flips deterministically on exit.

### Quick start (Phase 2)

Build the binary first if you haven't already: `make build` from the repo root.

```bash
# Terminal 1 — registry (same as Phase 1)
./bin/mcp-mesh-registry > /tmp/registry.log 2>&1 &

# Terminal 2 — event-aware provider (port 9122)
cd examples/jobs-java/event-aware-provider-java
MCP_MESH_REGISTRY_URL=http://localhost:8000 mvn spring-boot:run

# Terminal 3 — event-aware consumer (port 9123)
cd examples/jobs-java/event-aware-consumer-java
MCP_MESH_REGISTRY_URL=http://localhost:8000 mvn spring-boot:run
```

Drive the demo from a fourth terminal:

```bash
meshctl call --timeout 60 event-aware-consumer-java:drive_event_aware_task '{}'
```

Expected output shape:

```json
{
"job_id": "01HXY...",
"posted_seqs": [1, 2, 3],
"subscriber_status": "ok",
"observed_count": 3,
"observed_events": [
{"seq": 1, "payload": {"item": 1}},
{"seq": 2, "payload": {"item": 2}},
{"seq": 3, "payload": {"item": 3}}
],
"result": {"processed": 3, "status": "stopped"}
}
```

The producer's `recvEvent` consumed all 3 `work` events plus the
`stop` event (`processed: 3, status: "stopped"`); the consumer-side
`EventSubscription` observer mirrored the same 3 `work` events with
its own independent cursor (`observed_count: 3`).

For the full conceptual treatment of the event-channel surfaces, see
[`docs/concepts/jobs.md#event-injection`](../../docs/concepts/jobs.md#event-injection)
and [`#stream-subscription`](../../docs/concepts/jobs.md#stream-subscription).

## What's NOT in v2.2 yet

- Idempotency keys for retries (future).
- Resumable checkpoints (future).
- Webhook/SSE notifications (future).
- SPIRE-based caller-identity verification on `job_id` (future).

See `MESHJOB_DESIGN.org` at the repo root for the full roadmap.
56 changes: 56 additions & 0 deletions examples/jobs-java/event-aware-consumer-java/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>event-aware-consumer-java</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>MeshJob Event-Aware Consumer (Java)</name>
<description>Phase 2 MeshJob example — drives an event-aware job via postEvent + subscribeEvents</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.6</version>
<relativePath/>
</parent>

<properties>
<java.version>17</java.version>
<mcp-mesh.version>2.1.0</mcp-mesh.version>
</properties>

<!--
Depends on local mcp-mesh-* artifacts at version 2.1.0+ which carry the
v2.2 event-injection surfaces. Build them first via:
cd src/runtime/java && mvn install -DskipTests
Maven and IDEs then resolve them from your local ~/.m2/repository.
-->
<dependencies>
<dependency>
<groupId>io.mcp-mesh</groupId>
<artifactId>mcp-mesh-spring-boot-starter</artifactId>
<version>${mcp-mesh.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.example.eventawareconsumer.EventAwareConsumerApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package com.example.eventawareconsumer;

import io.mcpmesh.EventSubscription;
import io.mcpmesh.JobProxy;
import io.mcpmesh.MeshAgent;
import io.mcpmesh.MeshJob;
import io.mcpmesh.MeshJobs;
import io.mcpmesh.MeshJobSubmitter;
import io.mcpmesh.MeshTool;
import io.mcpmesh.Selector;
import io.mcpmesh.SubscribeOptions;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
* MeshJob Phase 2 — Java Consumer: drive an event-aware job (v2.2).
*
* <p>Demonstrates the three v2.2 event-channel surfaces from outside the
* running handler:
*
* <pre>{@code
* try (JobProxy proxy = submitter.submit(opts).get()) {
* // observer thread: walks EventSubscription, mirrors the stream
* // poster loop: fires 3 'work' events + 1 'stop' via MeshJobs.postEvent
* Object result = proxy.await(30.0);
* }
* }</pre>
*
* <p>The subscriber and the poster run concurrently. Each has its own
* cursor: the in-handler {@code recvEvent} cursor on the producer side is
* independent from the observer's {@link EventSubscription} cursor —
* both observe every {@code work} event the consumer posts.
*
* <p>Pair this consumer with {@code ../event-aware-provider-java}.
* Run after the provider is up:
*
* <pre>
* MCP_MESH_REGISTRY_URL=http://localhost:8000 mvn spring-boot:run
* </pre>
*/
@MeshAgent(
name = "event-aware-consumer-java",
version = "1.0.0",
description = "MeshJob v2.2 (Java) consumer — drives an event-aware job via postEvent + subscribeEvents",
port = 9123
)
@SpringBootApplication
public class EventAwareConsumerApplication {

public static void main(String[] args) {
SpringApplication.run(EventAwareConsumerApplication.class, args);
}

@MeshTool(
capability = "drive_event_aware_task",
description = "Submit an event-aware job, post 3 'work' events + 1 'stop', "
+ "mirror the stream via subscribeEvents, and return both halves.",
dependencies = @Selector(capability = "event_aware_long_task")
)
public Map<String, Object> driveEventAwareTask(MeshJob eventAwareLongTask) throws Exception {
if (!(eventAwareLongTask instanceof MeshJobSubmitter submitter)) {
return Map.of("error", "event_aware_long_task submitter not injected");
}

MeshJobSubmitter.SubmitOptions opts = new MeshJobSubmitter.SubmitOptions(
new LinkedHashMap<>(), null, 60, null, null);

try (JobProxy proxy = submitter.submit(opts).get()) {
String jobId = proxy.jobId();

// Brief wait so the producer claims the job + parks on
// recvEvent before the first event lands.
Thread.sleep(2000);

// Synchronized list so the subscriber thread and the main
// thread can both touch it safely.
List<Map<String, Object>> observed =
Collections.synchronizedList(new ArrayList<>());

List<Object> postedSeqs = new ArrayList<>();
String subscriberStatus;

// Subscriber runs on a daemon thread. Java has no async/await —
// try-with-resources on EventSubscription ensures the iterator
// closes cleanly when we leave the block.
try (EventSubscription subscription = MeshJobs.subscribeEvents(
jobId,
SubscribeOptions.builder()
.types(List.of("work"))
.longPoll(Duration.ofSeconds(5))
.build())) {
Thread subscriber = new Thread(() -> {
while (subscription.hasNext()) {
Map<String, Object> event = subscription.next();
Map<String, Object> entry = new LinkedHashMap<>();
entry.put("seq", event.get("seq"));
entry.put("payload", event.get("payload"));
observed.add(entry);
if (observed.size() >= 3) {
return;
}
}
}, "event-aware-subscriber");
subscriber.setDaemon(true);
subscriber.start();

// Poster: fire 3 'work' events ~500ms apart, then 1 'stop'.
for (int i = 1; i <= 3; i++) {
Thread.sleep(500);
Map<String, Object> receipt = MeshJobs.postEvent(
jobId, "work", Map.of("item", i));
postedSeqs.add(receipt.get("seq"));
}
MeshJobs.postEvent(jobId, "stop", Map.of());

// Bound the subscriber wait. The try-with-resources close()
// drops the iterator's "keep polling" flag on exit.
// On timeout, the daemon subscriber thread is still blocked inside
// proxy.listEvents()'s native FFI long-poll. EventSubscription.close()
// (run by the surrounding try-with-resources) flips a volatile flag
// that stops *future* long-polls but does NOT interrupt the in-flight
// one — the thread will exit at its next poll boundary (up to
// `longPoll` duration later). Daemon status keeps the JVM shutdown
// path clean; the leak window is bounded by `longPoll`.
subscriber.join(15_000L);
subscriberStatus = subscriber.isAlive() ? "timeout" : "ok";
}

// proxy.close() at end of this try block takes the JobProxy write
// lock; if the subscriber thread is still in flight (timeout branch
// above), close blocks until its read-locked listEvents call drains.
Object result = proxy.await(30.0);

Map<String, Object> response = new LinkedHashMap<>();
response.put("job_id", jobId);
response.put("posted_seqs", postedSeqs);
response.put("subscriber_status", subscriberStatus);
response.put("observed_count", observed.size());
response.put("observed_events", new ArrayList<>(observed));
response.put("result", result);
return response;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
server:
port: ${MCP_MESH_HTTP_PORT:9123}

spring:
main:
banner-mode: off

logging:
level:
root: INFO
io.mcpmesh: DEBUG
57 changes: 57 additions & 0 deletions examples/jobs-java/event-aware-provider-java/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>event-aware-provider-java</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>MeshJob Event-Aware Provider (Java)</name>
<description>Phase 2 MeshJob example — task=true producer that drains injected events</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.6</version>
<relativePath/>
</parent>

<properties>
<java.version>17</java.version>
<mcp-mesh.version>2.1.0</mcp-mesh.version>
</properties>

<!--
Depends on local mcp-mesh-* artifacts at version 2.1.0+ which carry the
v2.2 event-injection surfaces (MeshJobs.postEvent /
MeshJobs.subscribeEvents / JobController.recvEvent). Build them first via:
cd src/runtime/java && mvn install -DskipTests
Maven and IDEs then resolve them from your local ~/.m2/repository.
-->
<dependencies>
<dependency>
<groupId>io.mcp-mesh</groupId>
<artifactId>mcp-mesh-spring-boot-starter</artifactId>
<version>${mcp-mesh.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.example.eventawareprovider.EventAwareProviderApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
Loading
Loading