feat(ft546): add isolated shadow process
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
package com.ruoyi.shadow;
|
||||
|
||||
import com.ruoyi.shadow.config.ShadowKernelConfiguration;
|
||||
import com.ruoyi.shadow.config.ShadowRuntimeConfiguration;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration(exclude = {
|
||||
DataSourceAutoConfiguration.class,
|
||||
SecurityAutoConfiguration.class,
|
||||
UserDetailsServiceAutoConfiguration.class
|
||||
})
|
||||
@Import({ShadowRuntimeConfiguration.class, ShadowKernelConfiguration.class})
|
||||
@EnableScheduling
|
||||
public class Ft546ShadowApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Ft546ShadowApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.ruoyi.shadow.config;
|
||||
|
||||
import com.ruoyi.wcs.application.dispatch.DispatchCoordinator;
|
||||
import com.ruoyi.wcs.domain.dispatch.DefaultVehicleAssignmentPolicy;
|
||||
import com.ruoyi.wcs.domain.dispatch.VehicleAssignmentPolicy;
|
||||
import com.ruoyi.wcs.domain.shared.IdGenerator;
|
||||
import com.ruoyi.wcs.domain.traffic.AStarRoutePlanner;
|
||||
import com.ruoyi.wcs.domain.traffic.DeadlockGuard;
|
||||
import com.ruoyi.wcs.domain.traffic.RoutePlanner;
|
||||
import com.ruoyi.wcs.domain.traffic.WaitForGraph;
|
||||
import com.ruoyi.wcs.port.out.DispatchPolicy;
|
||||
import com.ruoyi.wcs.port.out.TrafficStateRepository;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class ShadowKernelConfiguration {
|
||||
@Bean
|
||||
public RoutePlanner shadowRoutePlanner() {
|
||||
return new AStarRoutePlanner();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public VehicleAssignmentPolicy shadowVehicleAssignmentPolicy(
|
||||
RoutePlanner routePlanner) {
|
||||
return new DefaultVehicleAssignmentPolicy(routePlanner,
|
||||
DispatchPolicy.NONE);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DeadlockGuard shadowDeadlockGuard(RoutePlanner routePlanner) {
|
||||
return new DeadlockGuard(new WaitForGraph(), routePlanner);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public IdGenerator shadowIdGenerator() {
|
||||
AtomicLong sequence = new AtomicLong();
|
||||
return () -> "SHADOW-" + sequence.incrementAndGet();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TrafficStateRepository disabledShadowTrafficStateRepository() {
|
||||
return commit -> {
|
||||
throw new IllegalStateException(
|
||||
"SHADOW_TRAFFIC_STATE_REPOSITORY_NOT_CONFIGURED");
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DispatchCoordinator shadowDispatchCoordinator(
|
||||
VehicleAssignmentPolicy assignmentPolicy,
|
||||
DeadlockGuard deadlockGuard,
|
||||
TrafficStateRepository trafficStateRepository,
|
||||
IdGenerator idGenerator) {
|
||||
return new DispatchCoordinator(assignmentPolicy, deadlockGuard,
|
||||
trafficStateRepository, idGenerator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.ruoyi.shadow.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "shadow")
|
||||
public class ShadowProperties {
|
||||
private boolean enabled;
|
||||
private final JdbcEndpoint source = new JdbcEndpoint();
|
||||
private final JdbcEndpoint evidence = new JdbcEndpoint();
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
public JdbcEndpoint getSource() { return source; }
|
||||
public JdbcEndpoint getEvidence() { return evidence; }
|
||||
|
||||
public static final class JdbcEndpoint {
|
||||
private String jdbcUrl;
|
||||
|
||||
public String getJdbcUrl() { return jdbcUrl; }
|
||||
public void setJdbcUrl(String jdbcUrl) { this.jdbcUrl = jdbcUrl; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.ruoyi.shadow.config;
|
||||
|
||||
import com.ruoyi.shadow.startup.ShadowStartupGate;
|
||||
import com.ruoyi.shadow.startup.ShadowStartupReport;
|
||||
import com.ruoyi.shadow.web.ShadowStatusController;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableConfigurationProperties(ShadowProperties.class)
|
||||
public class ShadowRuntimeConfiguration {
|
||||
@Bean
|
||||
public ShadowStartupGate shadowStartupGate() {
|
||||
return new ShadowStartupGate();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ShadowStartupReport shadowStartupReport(
|
||||
ShadowStartupGate gate, ApplicationContext context,
|
||||
Environment environment, ShadowProperties properties) {
|
||||
return gate.verify(context, environment, properties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ShadowStatusController shadowStatusController(
|
||||
ShadowStartupReport report) {
|
||||
return new ShadowStatusController(report);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.ruoyi.shadow.startup;
|
||||
|
||||
import com.ruoyi.shadow.config.ShadowProperties;
|
||||
import com.ruoyi.wcs.port.out.DeviceCommandGateway;
|
||||
import com.ruoyi.wcs.port.out.WmsNotificationGateway;
|
||||
import org.springframework.beans.factory.BeanIsNotAFactoryException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class ShadowStartupGate {
|
||||
public ShadowStartupReport verify(ApplicationContext context,
|
||||
Environment environment,
|
||||
ShadowProperties properties) {
|
||||
if (!properties.isEnabled()) {
|
||||
throw failure("SHADOW_MUST_BE_EXPLICITLY_ENABLED");
|
||||
}
|
||||
if (!Arrays.asList(environment.getActiveProfiles())
|
||||
.contains("shadow")) {
|
||||
throw failure("SHADOW_PROFILE_MUST_BE_ACTIVE");
|
||||
}
|
||||
requireText(properties.getSource().getJdbcUrl(),
|
||||
"SHADOW_SOURCE_JDBC_URL_REQUIRED");
|
||||
requireText(properties.getEvidence().getJdbcUrl(),
|
||||
"SHADOW_EVIDENCE_JDBC_URL_REQUIRED");
|
||||
requireLoopback(environment.getProperty("server.address",
|
||||
"127.0.0.1"));
|
||||
requireNoBeans(context, DeviceCommandGateway.class,
|
||||
"DEVICE_COMMAND_GATEWAY_FORBIDDEN_IN_SHADOW");
|
||||
requireNoBeans(context, WmsNotificationGateway.class,
|
||||
"WMS_NOTIFICATION_GATEWAY_FORBIDDEN_IN_SHADOW");
|
||||
requireNoForbiddenBeanPackage(context);
|
||||
Map<String, String> checks = new LinkedHashMap<String, String>();
|
||||
checks.put("SHADOW_ENABLED", "PASS");
|
||||
checks.put("SHADOW_PROFILE_ACTIVE", "PASS");
|
||||
checks.put("SOURCE_JDBC_CONFIGURED", "PASS");
|
||||
checks.put("EVIDENCE_JDBC_CONFIGURED", "PASS");
|
||||
checks.put("STATUS_ADDRESS_LOOPBACK", "PASS");
|
||||
checks.put("SIDE_EFFECT_BEANS_ABSENT", "PASS");
|
||||
checks.put("FORBIDDEN_BEAN_PACKAGES_ABSENT", "PASS");
|
||||
checks.put("SOURCE_PERMISSIONS_VERIFIED", "PENDING");
|
||||
checks.put("EVIDENCE_DATABASE_VERIFIED", "PENDING");
|
||||
return ShadowStartupReport.starting(Instant.now(), checks);
|
||||
}
|
||||
|
||||
private static void requireLoopback(String address) {
|
||||
String normalized = address == null ? "" : address.trim();
|
||||
if (!"127.0.0.1".equals(normalized)
|
||||
&& !"localhost".equalsIgnoreCase(normalized)
|
||||
&& !"::1".equals(normalized)) {
|
||||
throw failure("SHADOW_STATUS_ADDRESS_MUST_BE_LOOPBACK");
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> void requireNoBeans(ApplicationContext context,
|
||||
Class<T> type,
|
||||
String reasonCode) {
|
||||
if (!context.getBeansOfType(type).isEmpty()) {
|
||||
throw failure(reasonCode);
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireNoForbiddenBeanPackage(
|
||||
ApplicationContext context) {
|
||||
for (String beanName : context.getBeanDefinitionNames()) {
|
||||
requireAllowedBeanType(context.getType(beanName, false));
|
||||
requireAllowedBeanType(factoryBeanType(context, beanName));
|
||||
}
|
||||
}
|
||||
|
||||
private static Class<?> factoryBeanType(ApplicationContext context,
|
||||
String beanName) {
|
||||
try {
|
||||
return context.getType("&" + beanName, false);
|
||||
} catch (BeanIsNotAFactoryException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireAllowedBeanType(Class<?> beanType) {
|
||||
String typeName = beanType == null ? null : beanType.getName();
|
||||
if (typeName != null
|
||||
&& (typeName.startsWith("com.ruoyi.schedule.")
|
||||
|| typeName.startsWith("com.ruoyi.project.")
|
||||
|| typeName.startsWith("com.ruoyi.api."))) {
|
||||
throw failure("FORBIDDEN_PRODUCTION_BEAN_IN_SHADOW:"
|
||||
+ typeName);
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireText(String value, String reasonCode) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
throw failure(reasonCode);
|
||||
}
|
||||
}
|
||||
|
||||
private static IllegalStateException failure(String reasonCode) {
|
||||
return new IllegalStateException(reasonCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.ruoyi.shadow.startup;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class ShadowStartupReport {
|
||||
public enum Status { STARTING, READY, DEGRADED, STOPPED }
|
||||
|
||||
private final Status status;
|
||||
private final Instant checkedAt;
|
||||
private final Instant lastCompletedCycle;
|
||||
private final Long sourceLagMillis;
|
||||
private final long evidenceBacklog;
|
||||
private final Map<String, String> startupChecks;
|
||||
|
||||
private ShadowStartupReport(Status status, Instant checkedAt,
|
||||
Instant lastCompletedCycle,
|
||||
Long sourceLagMillis, long evidenceBacklog,
|
||||
Map<String, String> startupChecks) {
|
||||
this.status = status;
|
||||
this.checkedAt = checkedAt;
|
||||
this.lastCompletedCycle = lastCompletedCycle;
|
||||
this.sourceLagMillis = sourceLagMillis;
|
||||
this.evidenceBacklog = evidenceBacklog;
|
||||
this.startupChecks = Collections.unmodifiableMap(
|
||||
new LinkedHashMap<String, String>(startupChecks));
|
||||
}
|
||||
|
||||
public static ShadowStartupReport starting(Instant checkedAt,
|
||||
Map<String, String> checks) {
|
||||
return new ShadowStartupReport(Status.STARTING, checkedAt, null, null,
|
||||
0L, checks);
|
||||
}
|
||||
|
||||
public Status getStatus() { return status; }
|
||||
public Instant getCheckedAt() { return checkedAt; }
|
||||
public Instant getLastCompletedCycle() { return lastCompletedCycle; }
|
||||
public Long getSourceLagMillis() { return sourceLagMillis; }
|
||||
public long getEvidenceBacklog() { return evidenceBacklog; }
|
||||
public Map<String, String> getStartupChecks() { return startupChecks; }
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ruoyi.shadow.web;
|
||||
|
||||
import com.ruoyi.shadow.startup.ShadowStartupReport;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/shadow")
|
||||
public final class ShadowStatusController {
|
||||
private final ShadowStartupReport report;
|
||||
|
||||
public ShadowStatusController(ShadowStartupReport report) {
|
||||
this.report = report;
|
||||
}
|
||||
|
||||
@GetMapping("/status")
|
||||
public ShadowStartupReport status() {
|
||||
return report;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user