chore(auth): sync binding contract gate

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-07-31 00:32:44 +08:00
commit c6e390f643
7 changed files with 153 additions and 6 deletions

View file

@ -10,15 +10,29 @@ POSTGRES_PASSWORD="identity-v2-test-password"
POSTGRES_DB="identity_v2"
MAVEN_CACHE_DIR="${MAVEN_CACHE_DIR:-${HOME}/.m2}"
log() {
printf '[identity-binding-v2] %s\n' "$*"
}
cleanup() {
exit_code="$?"
if [[ "${exit_code}" -ne 0 ]]; then
log "failed with exit code ${exit_code}"
docker ps -a \
--filter "label=skillhub.test.run=${RUN_ID}" \
--format 'resource={{.Names}} status={{.Status}}' || true
docker logs "${POSTGRES_CONTAINER}" 2>&1 || true
fi
docker rm -f "${POSTGRES_CONTAINER}" >/dev/null 2>&1 || true
docker network rm "${NETWORK}" >/dev/null 2>&1 || true
}
trap cleanup EXIT
log "creating isolated Docker network ${NETWORK}"
docker network create \
--label "skillhub.test.run=${RUN_ID}" \
"${NETWORK}" >/dev/null
log "starting isolated PostgreSQL container ${POSTGRES_CONTAINER}"
docker run -d \
--name "${POSTGRES_CONTAINER}" \
--label "skillhub.test.run=${RUN_ID}" \
@ -31,6 +45,7 @@ docker run -d \
-p 127.0.0.1::5432 \
postgres:16-alpine >/dev/null
log "waiting for PostgreSQL readiness"
for _ in $(seq 1 60); do
if docker exec "${POSTGRES_CONTAINER}" \
pg_isready -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" \
@ -42,17 +57,23 @@ done
docker exec "${POSTGRES_CONTAINER}" \
pg_isready -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" \
>/dev/null
log "PostgreSQL is ready"
run_test() {
test_class="$1"
flyway_target="${2:-}"
java_version=""
if command -v java >/dev/null 2>&1; then
java_version="$(java -version 2>&1 | head -n 1)"
java_version="$(java -version 2>&1)"
fi
if [[ "${java_version}" == *'"21.'* ]]; then
host_port="$(docker port "${POSTGRES_CONTAINER}" 5432/tcp \
| sed -n 's/.*://p')"
if [[ -z "${host_port}" ]]; then
log "Docker did not publish a PostgreSQL host port"
return 1
fi
log "running ${test_class} with host Java 21"
(
cd "${REPO_ROOT}/server"
IDENTITY_BINDING_V2_POSTGRES_URL="jdbc:postgresql://127.0.0.1:${host_port}/${POSTGRES_DB}" \
@ -70,6 +91,7 @@ run_test() {
return
fi
log "running ${test_class} with containerized Java 21"
mkdir -p "${MAVEN_CACHE_DIR}"
docker run --rm \
--name "${RUN_ID}-java" \

View file

@ -1,14 +1,16 @@
package com.iflytek.skillhub.config;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import org.slf4j.MDC;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskDecorator;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* Enables asynchronous event handling and other background execution features used by the
* application module.
@ -26,7 +28,30 @@ public class AsyncConfig {
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("skillhub-event-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setTaskDecorator(mdcTaskDecorator());
executor.initialize();
return executor;
}
private TaskDecorator mdcTaskDecorator() {
return task -> {
Map<String, String> callerContext = MDC.getCopyOfContextMap();
return () -> {
Map<String, String> executorContext = MDC.getCopyOfContextMap();
try {
restoreMdc(callerContext);
task.run();
} finally {
restoreMdc(executorContext);
}
};
};
}
private void restoreMdc(Map<String, String> context) {
MDC.clear();
if (context != null) {
MDC.setContextMap(context);
}
}
}

View file

@ -60,4 +60,8 @@ public class SkillHubMetrics {
"operation", operation
).increment();
}
public void incrementSearchRebuildFailure() {
meterRegistry.counter("skillhub.search.rebuild.failure").increment();
}
}

View file

@ -1,5 +1,6 @@
package com.iflytek.skillhub.service;
import com.iflytek.skillhub.metrics.SkillHubMetrics;
import com.iflytek.skillhub.search.SearchRebuildService;
import java.util.List;
import org.slf4j.Logger;
@ -15,9 +16,12 @@ public class LabelSearchSyncService {
private static final Logger log = LoggerFactory.getLogger(LabelSearchSyncService.class);
private final SearchRebuildService searchRebuildService;
private final SkillHubMetrics metrics;
public LabelSearchSyncService(SearchRebuildService searchRebuildService) {
public LabelSearchSyncService(SearchRebuildService searchRebuildService,
SkillHubMetrics metrics) {
this.searchRebuildService = searchRebuildService;
this.metrics = metrics;
}
@Async("skillhubEventExecutor")
@ -25,6 +29,7 @@ public class LabelSearchSyncService {
try {
searchRebuildService.rebuildBySkill(skillId);
} catch (RuntimeException ex) {
metrics.incrementSearchRebuildFailure();
log.error("Failed to rebuild search document for skill {}", skillId, ex);
}
}
@ -41,6 +46,7 @@ public class LabelSearchSyncService {
try {
searchRebuildService.rebuildBySkill(skillId);
} catch (RuntimeException ex) {
metrics.incrementSearchRebuildFailure();
log.error("Failed to rebuild search document for skill {} after label change", skillId, ex);
}
}

View file

@ -2,9 +2,13 @@ package com.iflytek.skillhub.config;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
import org.slf4j.MDC;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
class AsyncConfigTest {
@ -13,4 +17,26 @@ class AsyncConfigTest {
assertThat(AsyncConfig.class).hasAnnotation(EnableAsync.class);
assertThat(AsyncConfig.class).hasAnnotation(EnableScheduling.class);
}
@Test
void skillhubEventExecutor_propagatesAndClearsMdc() throws Exception {
ThreadPoolTaskExecutor executor =
(ThreadPoolTaskExecutor) new AsyncConfig().skillhubEventExecutor();
try {
MDC.put("requestId", "req-597");
CompletableFuture<String> propagatedRequestId = new CompletableFuture<>();
executor.execute(() -> propagatedRequestId.complete(MDC.get("requestId")));
MDC.clear();
assertThat(propagatedRequestId.get(5, TimeUnit.SECONDS)).isEqualTo("req-597");
CompletableFuture<String> nextRequestId = new CompletableFuture<>();
executor.execute(() -> nextRequestId.complete(MDC.get("requestId")));
assertThat(nextRequestId.get(5, TimeUnit.SECONDS)).isNull();
} finally {
MDC.clear();
executor.shutdown();
}
}
}

View file

@ -39,6 +39,7 @@ class PrometheusEndpointTest {
skillHubMetrics.incrementUserRegister();
skillHubMetrics.recordLocalLogin(true);
skillHubMetrics.incrementSkillPublish("global", "PENDING_REVIEW");
skillHubMetrics.incrementSearchRebuildFailure();
assertThat(environment.getProperty("management.endpoints.web.exposure.include"))
.doesNotContain("prometheus")
@ -54,5 +55,8 @@ class PrometheusEndpointTest {
.tag("status", "PENDING_REVIEW")
.counter()
.count()).isEqualTo(1.0d);
assertThat(meterRegistry.get("skillhub.search.rebuild.failure")
.counter()
.count()).isEqualTo(1.0d);
}
}

View file

@ -1,13 +1,19 @@
package com.iflytek.skillhub.service;
import com.iflytek.skillhub.metrics.SkillHubMetrics;
import com.iflytek.skillhub.search.SearchRebuildService;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
class LabelSearchSyncServiceTest {
@ -15,7 +21,8 @@ class LabelSearchSyncServiceTest {
@Test
void rebuildSkillsShouldSkipNullsAndDuplicatesWhileProcessingLargeLists() {
SearchRebuildService rebuildService = mock(SearchRebuildService.class);
LabelSearchSyncService service = new LabelSearchSyncService(rebuildService);
SkillHubMetrics metrics = mock(SkillHubMetrics.class);
LabelSearchSyncService service = new LabelSearchSyncService(rebuildService, metrics);
List<Long> skillIds = new ArrayList<>();
skillIds.add(null);
for (long i = 1; i <= 120; i++) {
@ -30,5 +37,58 @@ class LabelSearchSyncServiceTest {
verify(rebuildService).rebuildBySkill(i);
}
verifyNoMoreInteractions(rebuildService);
verifyNoInteractions(metrics);
}
@Test
void rebuildSkillFailureShouldIncrementMetric() {
SearchRebuildService rebuildService = mock(SearchRebuildService.class);
doThrow(new IllegalStateException("search unavailable"))
.when(rebuildService)
.rebuildBySkill(42L);
contextRunner(rebuildService).run(context -> {
LabelSearchSyncService service = context.getBean(LabelSearchSyncService.class);
SimpleMeterRegistry meterRegistry = context.getBean(SimpleMeterRegistry.class);
service.rebuildSkill(42L);
assertThat(meterRegistry.get("skillhub.search.rebuild.failure").counter().count())
.isEqualTo(1.0d);
});
}
@Test
void rebuildSkillsShouldCountEachFailureAndContinue() {
SearchRebuildService rebuildService = mock(SearchRebuildService.class);
doThrow(new IllegalStateException("search unavailable"))
.when(rebuildService)
.rebuildBySkill(2L);
doThrow(new IllegalStateException("search unavailable"))
.when(rebuildService)
.rebuildBySkill(3L);
contextRunner(rebuildService).run(context -> {
LabelSearchSyncService service = context.getBean(LabelSearchSyncService.class);
SimpleMeterRegistry meterRegistry = context.getBean(SimpleMeterRegistry.class);
service.rebuildSkills(List.of(1L, 2L, 3L, 4L));
assertThat(meterRegistry.get("skillhub.search.rebuild.failure").counter().count())
.isEqualTo(2.0d);
verify(rebuildService).rebuildBySkill(1L);
verify(rebuildService).rebuildBySkill(2L);
verify(rebuildService).rebuildBySkill(3L);
verify(rebuildService).rebuildBySkill(4L);
verifyNoMoreInteractions(rebuildService);
});
}
private ApplicationContextRunner contextRunner(SearchRebuildService rebuildService) {
return new ApplicationContextRunner()
.withBean(SearchRebuildService.class, () -> rebuildService)
.withBean(SimpleMeterRegistry.class)
.withBean(SkillHubMetrics.class)
.withBean(LabelSearchSyncService.class);
}
}