fix: harden jt808 tdengine ingestion
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
lingniu
2026-06-29 19:53:08 +08:00
parent d1748fcc2f
commit 12de83e37f
17 changed files with 588 additions and 189 deletions

View File

@@ -36,16 +36,44 @@ public class RateLimitInterceptor implements IngestInterceptor, Ordered {
@Override
public boolean before(RawFrame frame, IngestContext ctx) {
String vin = frame.sourceMeta().getOrDefault("vin", "unknown");
// VIN 粒度隔离,避免某一辆车的高频/异常上报挤占其他车辆处理额度。
RateLimiter rl = limiters.get(vin, k -> RateLimiter.of("vin-" + k, defaultConfig));
String limiterKey = limiterKey(frame);
// 车辆粒度隔离VIN 可用时按 VINJT808 等未映射 VIN 的协议按 vehicleKey/phone 兜底,
// 避免所有 unknown 终端挤在同一个限流桶里。
RateLimiter rl = limiters.get(limiterKey, k -> RateLimiter.of("vehicle-" + k, defaultConfig));
if (!rl.acquirePermission()) {
ctx.abort("rate-limited:" + vin);
ctx.abort("rate-limited:" + limiterKey);
return false;
}
return true;
}
private static String limiterKey(RawFrame frame) {
String vin = meta(frame, "vin");
if (isKnown(vin)) {
return "vin:" + vin;
}
String vehicleKey = meta(frame, "vehicleKey");
if (isKnown(vehicleKey)) {
return "vehicleKey:" + vehicleKey;
}
String phone = meta(frame, "phone");
if (isKnown(phone)) {
return "phone:" + phone;
}
return "unknown";
}
private static String meta(RawFrame frame, String key) {
if (frame == null || frame.sourceMeta() == null) {
return "";
}
return frame.sourceMeta().getOrDefault(key, "").trim();
}
private static boolean isKnown(String value) {
return value != null && !value.isBlank() && !"unknown".equalsIgnoreCase(value.trim());
}
@Override
public int getOrder() {
return 200;

View File

@@ -0,0 +1,55 @@
package com.lingniu.ingest.core.pipeline.builtin;
import com.lingniu.ingest.api.ProtocolId;
import com.lingniu.ingest.api.pipeline.IngestContext;
import com.lingniu.ingest.api.pipeline.RawFrame;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class RateLimitInterceptorTest {
@Test
void unknownJt808VinUsesPhoneAsIndependentLimiterKey() {
RateLimitInterceptor interceptor = new RateLimitInterceptor(1, 100);
IngestContext first = new IngestContext("trace-1");
IngestContext second = new IngestContext("trace-2");
boolean firstAllowed = interceptor.before(frame("unknown", "13079970001"), first);
boolean secondAllowed = interceptor.before(frame("unknown", "13079970002"), second);
assertThat(firstAllowed).isTrue();
assertThat(secondAllowed).isTrue();
assertThat(first.aborted()).isFalse();
assertThat(second.aborted()).isFalse();
}
@Test
void sameJt808PhoneStillSharesLimiterWhenVinIsUnknown() {
RateLimitInterceptor interceptor = new RateLimitInterceptor(1, 100);
IngestContext first = new IngestContext("trace-1");
IngestContext second = new IngestContext("trace-2");
boolean firstAllowed = interceptor.before(frame("unknown", "13079970001"), first);
boolean secondAllowed = interceptor.before(frame("unknown", "13079970001"), second);
assertThat(firstAllowed).isTrue();
assertThat(secondAllowed).isFalse();
assertThat(second.aborted()).isTrue();
assertThat(second.abortReason()).isEqualTo("rate-limited:phone:13079970001");
}
private static RawFrame frame(String vin, String phone) {
return new RawFrame(
ProtocolId.JT808,
0x0200,
0,
new Object(),
new byte[]{0x01},
Map.of("vin", vin, "phone", phone),
Instant.parse("2026-06-29T00:00:00Z"));
}
}