actionsFor(long ruleId) {
+ return jdbc.query(
+ "SELECT basic_action, action_details, seq AS sequence FROM rule_actions "
+ + "WHERE rule_id = ? ORDER BY seq",
+ (rs, i) -> new RuleAction(rs.getString("basic_action"),
+ rs.getString("action_details"), rs.getInt("sequence")),
+ ruleId);
+ }
+}
diff --git a/async-config-poll/src/main/java/com/example/asyncconfig/rules/RulesResource.java b/async-config-poll/src/main/java/com/example/asyncconfig/rules/RulesResource.java
new file mode 100644
index 00000000..c1ff52bf
--- /dev/null
+++ b/async-config-poll/src/main/java/com/example/asyncconfig/rules/RulesResource.java
@@ -0,0 +1,70 @@
+package com.example.asyncconfig.rules;
+
+import java.util.List;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+/**
+ * Rule-engine endpoint: GET /rules/{useCase}. Requires the X-Tenant-Id and
+ * X-Agent-Id headers and returns the ordered rules for the (useCase, tenant),
+ * read from MySQL, e.g.:
+ *
+ * [{"use_case":"ORDER_FLOW","tenant":"ACME","rules":[
+ * {"rule_id":14,"constraints":"...","rule_type":"POST","actions":[
+ * {"basic_action":"...","action_details":"{}","sequence":1}]}]}]
+ *
+ */
+@Component
+@Path("/rules")
+public class RulesResource {
+
+ private final RuleDao ruleDao;
+ private final ObjectMapper mapper;
+ private final long delayMs;
+
+ @Autowired
+ public RulesResource(RuleDao ruleDao, ObjectMapper mapper,
+ @Value("${app.rules.delayMs:0}") long delayMs) {
+ this.ruleDao = ruleDao;
+ this.mapper = mapper;
+ this.delayMs = delayMs;
+ }
+
+ @GET
+ @Path("/{useCase}")
+ @Produces("application/json;charset=utf-8")
+ public Response rules(@PathParam("useCase") String useCase,
+ @HeaderParam("X-Tenant-Id") String tenantId,
+ @HeaderParam("X-Agent-Id") String agentId) throws Exception {
+ if (isBlank(tenantId) || isBlank(agentId)) {
+ return Response.status(Response.Status.BAD_REQUEST)
+ .type("application/json")
+ .entity("{\"error\":\"X-Tenant-Id and X-Agent-Id headers are required\"}")
+ .build();
+ }
+ if (delayMs > 0) {
+ try {
+ Thread.sleep(delayMs); // widen the request window for the fast-poller demo
+ } catch (InterruptedException ie) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ List result = ruleDao.rulesFor(useCase, tenantId);
+ String json = mapper.writeValueAsString(result);
+ return Response.ok(json).build();
+ }
+
+ private static boolean isBlank(String s) {
+ return s == null || s.trim().isEmpty();
+ }
+}
diff --git a/async-config-poll/src/main/java/com/example/asyncconfig/rules/UseCaseRules.java b/async-config-poll/src/main/java/com/example/asyncconfig/rules/UseCaseRules.java
new file mode 100644
index 00000000..8f86e233
--- /dev/null
+++ b/async-config-poll/src/main/java/com/example/asyncconfig/rules/UseCaseRules.java
@@ -0,0 +1,23 @@
+package com.example.asyncconfig.rules;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/** One (use_case, tenant) group with its ordered rules. */
+public class UseCaseRules {
+ @JsonProperty("use_case")
+ private String useCase;
+ private String tenant;
+ private List rules;
+
+ public UseCaseRules(String useCase, String tenant, List rules) {
+ this.useCase = useCase;
+ this.tenant = tenant;
+ this.rules = rules;
+ }
+
+ public String getUseCase() { return useCase; }
+ public String getTenant() { return tenant; }
+ public List getRules() { return rules; }
+}
diff --git a/async-config-poll/src/main/resources/application.yml b/async-config-poll/src/main/resources/application.yml
new file mode 100644
index 00000000..6c12965c
--- /dev/null
+++ b/async-config-poll/src/main/resources/application.yml
@@ -0,0 +1,35 @@
+server:
+ port: 8080
+
+spring:
+ datasource:
+ url: jdbc:mysql://127.0.0.1:3306/ruledb?useSSL=false
+ username: app
+ password: app
+ # driver auto-detected: com.mysql.jdbc.Driver (Connector/J 5.1.x, managed by SB 1.5).
+ # Keep the pool tiny with no validation queries so MySQL traffic is lean and
+ # deterministic — important for a clean Keploy record/replay.
+ tomcat:
+ initial-size: 1
+ max-active: 2
+ max-idle: 1
+ min-idle: 1
+ test-on-borrow: false
+ test-on-return: false
+ test-while-idle: false
+
+app:
+ config:
+ # The config service the app boot-fetches from and then watches.
+ baseUrl: http://127.0.0.1:9100
+ # Background watch long-poll interval. Lower it (e.g. WATCH_INTERVAL_MS=150)
+ # to make a poll land in the middle of a testcase during replay.
+ watchIntervalMs: ${WATCH_INTERVAL_MS:700}
+ rules:
+ # Artificial delay on GET /rules/{useCase}; widen it (RULES_DELAY_MS) so a
+ # fast watch poll can land while a testcase is still executing.
+ delayMs: ${RULES_DELAY_MS:0}
+
+logging:
+ level:
+ root: INFO