From e3dc7aedf620eda5216e5022db3e4a92b5b33165 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 25 Jun 2026 11:05:07 +0800 Subject: [PATCH] docs: add redis stream demo implementation plan --- ...-06-25-redis-stream-demo-implementation.md | 595 ++++++++++++++++++ 1 file changed, 595 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-25-redis-stream-demo-implementation.md diff --git a/docs/superpowers/plans/2026-06-25-redis-stream-demo-implementation.md b/docs/superpowers/plans/2026-06-25-redis-stream-demo-implementation.md new file mode 100644 index 0000000..7e8e620 --- /dev/null +++ b/docs/superpowers/plans/2026-06-25-redis-stream-demo-implementation.md @@ -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 + + + 4.0.0 + + com.njcn.demo + redis-stream-starter-demo + 1.0.0-SNAPSHOT + jar + + redis-stream-starter-demo + Demo application for redis-stream-springboot-starter + + + 1.8 + 8 + 8 + UTF-8 + UTF-8 + 2.3.12.RELEASE + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + com.njcn + redis-stream-springboot-starter + 1.0.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + UTF-8 + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + + +``` + +- [ ] **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 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 { + + 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 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.