Compare commits

..

3 Commits

Author SHA1 Message Date
root
373b488249 Merge remote-tracking branch 'origin/main' 2026-06-25 14:47:01 +08:00
root
e3dc7aedf6 docs: add redis stream demo implementation plan 2026-06-25 11:05:07 +08:00
root
eb70ffabf2 docs: add redis stream demo design 2026-06-25 11:02:01 +08:00
2 changed files with 711 additions and 0 deletions

View File

@@ -0,0 +1,595 @@
# Redis Stream Demo Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build an independent Spring Boot demo project that imports `redis-stream-springboot-starter` and verifies real Redis Stream publish/subscribe behavior.
**Architecture:** The demo is a downstream Maven Spring Boot application under `C:\code\hatch\redis-stream-starter-demo`. It depends on the starter from the local Maven repository, sends a `DemoMessage` with `RedisStreamEnhanceTemplate`, and consumes it with an `EnhanceStreamConsumerHandler` implementation. Integration testing uses the provided Redis service when reachable and skips when unreachable.
**Tech Stack:** Java 8, Maven, Spring Boot `2.3.12.RELEASE`, JUnit 5, `redis-stream-springboot-starter:1.0.0-SNAPSHOT`, Redis Stream.
---
## File Structure
- Create: `C:\code\hatch\redis-stream-starter-demo\pom.xml`
- Maven project definition, Spring Boot 2.3.12 dependency management, starter dependency, test setup.
- Create: `C:\code\hatch\redis-stream-starter-demo\.gitignore`
- Keeps build output and IDE files out of the demo project.
- Create: `C:\code\hatch\redis-stream-starter-demo\src\main\resources\application.yml`
- Redis host/port defaults and overridable password environment variable.
- Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoApplication.java`
- Spring Boot entry point.
- Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoMessage.java`
- Demo message payload extending `StreamBaseMessage`.
- Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoRuntimeInbox.java`
- In-memory queue shared by runtime logging and integration tests.
- Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoProducer.java`
- Sends demo messages through the starter template.
- Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoConsumer.java`
- Starter consumer handler for `demo-topic`.
- Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoStartupRunner.java`
- Sends one demo message when running the app manually.
- Create: `C:\code\hatch\redis-stream-starter-demo\src\test\java\com\njcn\demo\redisstream\DemoRoundTripIntegrationTest.java`
- Real Redis publish/subscribe integration test.
## Task 1: Prepare Build Scaffolding
**Files:**
- Create: `C:\code\hatch\redis-stream-starter-demo\pom.xml`
- Create: `C:\code\hatch\redis-stream-starter-demo\.gitignore`
- Create: `C:\code\hatch\redis-stream-starter-demo\src\main\resources\application.yml`
- [ ] **Step 1: Install the starter into the local Maven repository**
Run from the starter repository:
```bash
mvn install
```
Expected: `BUILD SUCCESS`, with `redis-stream-springboot-starter-1.0.0-SNAPSHOT.jar` installed into the local Maven repository.
- [ ] **Step 2: Create demo project directories**
Run:
```powershell
New-Item -ItemType Directory -Force `
'C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream', `
'C:\code\hatch\redis-stream-starter-demo\src\main\resources', `
'C:\code\hatch\redis-stream-starter-demo\src\test\java\com\njcn\demo\redisstream'
```
Expected: all three directories exist.
- [ ] **Step 3: Create `pom.xml`**
Write this exact content:
```xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.njcn.demo</groupId>
<artifactId>redis-stream-starter-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>redis-stream-starter-demo</name>
<description>Demo application for redis-stream-springboot-starter</description>
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.12.RELEASE</spring-boot.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>redis-stream-springboot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</build>
</project>
```
- [ ] **Step 4: Create `.gitignore`**
Write this exact content:
```gitignore
target/
.idea/
*.iml
.classpath
.project
.settings/
```
- [ ] **Step 5: Create `application.yml`**
Write this exact content:
```yaml
spring:
redis:
host: ${DEMO_REDIS_HOST:192.168.1.22}
port: ${DEMO_REDIS_PORT:12379}
password: ${DEMO_REDIS_PASSWORD:}
redis-stream:
env-isolation: true
env: demo
consumer-name: demo-consumer
read-count: 10
block-ms: 500
max-retry: 1
```
- [ ] **Step 6: Verify build scaffolding resolves dependencies**
Run from the demo project:
```bash
mvn test -DskipTests
```
Expected: `BUILD SUCCESS`.
## Task 2: Write the Round-Trip Integration Test First
**Files:**
- Create: `C:\code\hatch\redis-stream-starter-demo\src\test\java\com\njcn\demo\redisstream\DemoRoundTripIntegrationTest.java`
- [ ] **Step 1: Write the failing test**
Write this exact content:
```java
package com.njcn.demo.redisstream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.stream.RecordId;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ExtendWith(DemoRoundTripIntegrationTest.RedisReachableCondition.class)
@SpringBootTest
class DemoRoundTripIntegrationTest {
@Autowired
private DemoProducer producer;
@Autowired
private DemoRuntimeInbox inbox;
@Test
void publishesAndConsumesDemoMessage() throws Exception {
inbox.clear();
String payload = "integration-" + System.currentTimeMillis();
DemoMessage sent = new DemoMessage();
sent.setPayload(payload);
sent.setSource("integration-test");
sent.setTag("demo");
RecordId recordId = producer.send(sent);
DemoMessage received = inbox.poll(10, TimeUnit.SECONDS);
assertNotNull(recordId, "XADD should return a Redis stream record id");
assertNotNull(received, "consumer should receive the sent message within 10 seconds");
assertEquals(sent.getKey(), received.getKey(), "message key should round-trip");
assertEquals(payload, received.getPayload(), "payload should round-trip");
assertEquals("integration-test", received.getSource(), "source should round-trip");
}
static class RedisReachableCondition implements ExecutionCondition {
@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
String host = System.getProperty("spring.redis.host",
System.getenv().getOrDefault("DEMO_REDIS_HOST", "192.168.1.22"));
int port = Integer.parseInt(System.getProperty("spring.redis.port",
System.getenv().getOrDefault("DEMO_REDIS_PORT", "12379")));
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(host, port), 500);
return ConditionEvaluationResult.enabled("redis reachable at " + host + ":" + port);
} catch (Exception e) {
return ConditionEvaluationResult.disabled("redis unreachable at " + host + ":" + port);
}
}
}
}
```
- [ ] **Step 2: Run the test and verify it fails because application classes do not exist**
Run from the demo project with the Redis password supplied through the process environment:
```bash
mvn test -Dtest=DemoRoundTripIntegrationTest
```
Expected: compilation failure mentioning missing symbols such as `DemoProducer`, `DemoRuntimeInbox`, and `DemoMessage`.
## Task 3: Implement Minimal Demo Application Classes
**Files:**
- Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoApplication.java`
- Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoMessage.java`
- Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoRuntimeInbox.java`
- Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoProducer.java`
- Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoConsumer.java`
- [ ] **Step 1: Create `DemoApplication.java`**
Write this exact content:
```java
package com.njcn.demo.redisstream;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
```
- [ ] **Step 2: Create `DemoMessage.java`**
Write this exact content:
```java
package com.njcn.demo.redisstream;
import com.njcn.middle.stream.domain.StreamBaseMessage;
public class DemoMessage extends StreamBaseMessage {
private String payload;
public String getPayload() {
return payload;
}
public void setPayload(String payload) {
this.payload = payload;
}
}
```
- [ ] **Step 3: Create `DemoRuntimeInbox.java`**
Write this exact content:
```java
package com.njcn.demo.redisstream;
import org.springframework.stereotype.Component;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
@Component
public class DemoRuntimeInbox {
private final BlockingQueue<DemoMessage> messages = new LinkedBlockingQueue<>();
public void add(DemoMessage message) {
messages.add(message);
}
public DemoMessage poll(long timeout, TimeUnit unit) throws InterruptedException {
return messages.poll(timeout, unit);
}
public void clear() {
messages.clear();
}
}
```
- [ ] **Step 4: Create `DemoProducer.java`**
Write this exact content:
```java
package com.njcn.demo.redisstream;
import com.njcn.middle.stream.template.RedisStreamEnhanceTemplate;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.stereotype.Service;
@Service
public class DemoProducer {
public static final String TOPIC = "demo-topic";
private final RedisStreamEnhanceTemplate streamTemplate;
public DemoProducer(RedisStreamEnhanceTemplate streamTemplate) {
this.streamTemplate = streamTemplate;
}
public RecordId send(DemoMessage message) {
return streamTemplate.send(TOPIC, message, false);
}
}
```
- [ ] **Step 5: Create `DemoConsumer.java`**
Write this exact content:
```java
package com.njcn.demo.redisstream;
import com.njcn.middle.stream.handler.EnhanceStreamConsumerHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class DemoConsumer extends EnhanceStreamConsumerHandler<DemoMessage> {
public static final String GROUP = "demo-group";
private static final Logger log = LoggerFactory.getLogger(DemoConsumer.class);
private final DemoRuntimeInbox inbox;
public DemoConsumer(DemoRuntimeInbox inbox) {
this.inbox = inbox;
}
@Override
public String topic() {
return DemoProducer.TOPIC;
}
@Override
public String group() {
return GROUP;
}
@Override
public Class<DemoMessage> messageType() {
return DemoMessage.class;
}
@Override
public void handleMessage(DemoMessage message) {
log.info("Consumed Redis Stream demo message: key={}, payload={}, source={}, tag={}",
message.getKey(), message.getPayload(), message.getSource(), message.getTag());
inbox.add(message);
}
@Override
public boolean isRetry() {
return false;
}
@Override
public boolean throwException() {
return false;
}
}
```
- [ ] **Step 6: Run the integration test and verify it passes**
Run from the demo project with the Redis password supplied through the process environment:
```bash
mvn test -Dtest=DemoRoundTripIntegrationTest
```
Expected: `BUILD SUCCESS`, and the test reports one successful test when Redis is reachable.
## Task 4: Add Manual Startup Publisher
**Files:**
- Create: `C:\code\hatch\redis-stream-starter-demo\src\main\java\com\njcn\demo\redisstream\DemoStartupRunner.java`
- [ ] **Step 1: Write the failing startup-runner test by extending the integration test**
Modify `DemoRoundTripIntegrationTest.java` to include this field and test method:
```java
@Autowired
private DemoStartupRunner startupRunner;
@Test
void startupRunnerPublishesDemoMessage() throws Exception {
inbox.clear();
startupRunner.run();
DemoMessage received = inbox.poll(10, TimeUnit.SECONDS);
assertNotNull(received, "startup runner should publish a message");
assertEquals("startup-runner", received.getSource(), "startup runner should set source");
assertEquals("hello redis stream starter", received.getPayload(), "startup runner payload should match");
}
```
The full test class should still keep the first `publishesAndConsumesDemoMessage` test unchanged.
- [ ] **Step 2: Run the test and verify it fails because `DemoStartupRunner` does not exist**
Run:
```bash
mvn test -Dtest=DemoRoundTripIntegrationTest
```
Expected: compilation failure mentioning missing symbol `DemoStartupRunner`.
- [ ] **Step 3: Create `DemoStartupRunner.java`**
Write this exact content:
```java
package com.njcn.demo.redisstream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.stereotype.Component;
@Component
public class DemoStartupRunner implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(DemoStartupRunner.class);
private final DemoProducer producer;
public DemoStartupRunner(DemoProducer producer) {
this.producer = producer;
}
@Override
public void run(ApplicationArguments args) {
run();
}
public RecordId run() {
DemoMessage message = new DemoMessage();
message.setPayload("hello redis stream starter");
message.setSource("startup-runner");
message.setTag("demo");
RecordId recordId = producer.send(message);
log.info("Published Redis Stream demo message: recordId={}, key={}, payload={}",
recordId, message.getKey(), message.getPayload());
return recordId;
}
}
```
- [ ] **Step 4: Run the integration test and verify both tests pass**
Run:
```bash
mvn test -Dtest=DemoRoundTripIntegrationTest
```
Expected: `BUILD SUCCESS`, with two successful tests when Redis is reachable.
## Task 5: Final Verification
**Files:**
- No new files.
- [ ] **Step 1: Run the starter build**
Run from the starter repository:
```bash
mvn install
```
Expected: `BUILD SUCCESS`.
- [ ] **Step 2: Run all demo tests**
Run from the demo project with the Redis password supplied through the process environment:
```bash
mvn test
```
Expected: `BUILD SUCCESS`.
- [ ] **Step 3: Run the demo application**
Run from the demo project with the Redis password supplied through the process environment:
```bash
mvn spring-boot:run
```
Expected: startup logs show a published Redis Stream message and a consumed Redis Stream message. Stop the process after the message is consumed.
- [ ] **Step 4: Capture final file status**
Run from the starter repository:
```bash
git status --short
```
Expected: only the implementation plan file is uncommitted unless execution commits it separately.
## Self-Review
- Spec coverage: Tasks cover local starter installation, independent demo project creation, Redis configuration, producer, consumer, startup publishing, integration testing, and manual runtime verification.
- Placeholder scan: No TBD/TODO markers are used. Redis password is intentionally not written into this committed plan.
- Type consistency: `DemoMessage`, `DemoProducer`, `DemoConsumer`, `DemoRuntimeInbox`, `DemoStartupRunner`, and `DemoRoundTripIntegrationTest` use matching package names and method signatures.

View File

@@ -0,0 +1,116 @@
# Redis Stream Starter Demo Design
## Goal
Create an independent Spring Boot demo project at `C:\code\hatch\redis-stream-starter-demo` that imports the local `redis-stream-springboot-starter` and verifies Redis Stream message publish and subscribe behavior against the provided Redis service.
## Context
The current repository is a Maven single-module Spring Boot starter:
- Group/artifact/version: `com.njcn:redis-stream-springboot-starter:1.0.0-SNAPSHOT`
- Spring Boot baseline: `2.3.12.RELEASE`
- Java baseline: 8
- Auto-configuration entry: `META-INF/spring.factories`
- Producer API: `RedisStreamEnhanceTemplate#send(topic, message, compress)`
- Consumer API: subclass `EnhanceStreamConsumerHandler<T>`
## Demo Project Location
The demo project will be created outside this repository at:
```text
C:\code\hatch\redis-stream-starter-demo
```
This keeps the starter repository unchanged except for this design document and allows the demo to behave like a real downstream application.
## Dependency Flow
Before building the demo, install the starter into the local Maven repository:
```bash
mvn install
```
The demo `pom.xml` will then depend on:
```xml
<dependency>
<groupId>com.njcn</groupId>
<artifactId>redis-stream-springboot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
```
The demo will use Spring Boot `2.3.12.RELEASE` to match the starter's dependency baseline.
## Redis Configuration
The demo will use the provided Redis Stream service:
- Host: configured as `192.168.1.22`
- Port: configured as `12379`
- Password: configured in the local demo application config, not copied into this design document
The demo should allow overriding these values with environment variables or JVM system properties so the sample remains reusable.
## Runtime Components
The demo will include:
- `DemoApplication`: Spring Boot entry point.
- `DemoMessage`: message payload extending `StreamBaseMessage`.
- `DemoProducer`: injects `RedisStreamEnhanceTemplate` and publishes a message.
- `DemoConsumer`: extends `EnhanceStreamConsumerHandler<DemoMessage>` and subscribes to the same topic and group.
- `DemoStartupRunner`: sends one message after the application starts and logs the returned Redis `RecordId`.
The consumer will log received message content and store consumed messages in an in-memory queue for integration test assertions.
## Message Flow
1. Application starts and auto-configuration creates the starter beans.
2. `DemoStartupRunner` creates a `DemoMessage`.
3. `DemoProducer` calls `RedisStreamEnhanceTemplate#send("demo-topic", message, false)`.
4. The starter writes the message to Redis Stream with `XADD`.
5. The starter listener reads the message through `XREADGROUP`.
6. `DemoConsumer#handleMessage` receives the deserialized `DemoMessage`.
7. The starter ACKs the message after successful handling.
## Testing
Add an integration test that:
- Starts the demo Spring context.
- Uses the real Redis service when reachable.
- Sends a unique test message.
- Waits for the consumer to receive it.
- Asserts that the received message payload and key match the sent message.
If Redis is unreachable, the integration test should be skipped instead of failed. This keeps local builds deterministic while still validating the real publish/subscribe flow when the service is available.
## Error Handling
The demo consumer will return `false` for `isRetry()` so test failures are surfaced through assertions instead of retry loops. Unexpected handler exceptions will be logged by the starter path and ACKed according to the existing handler behavior.
## Verification Commands
From the starter repository:
```bash
mvn install
```
From the demo project:
```bash
mvn test
mvn spring-boot:run
```
## Out of Scope
- Publishing the starter to Nexus.
- Changing starter production code.
- Adding Docker or Testcontainers.
- Building a UI or HTTP API for the demo.