feat: refresh mysql vehicle identity cache
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -1,29 +1,43 @@
|
||||
package com.lingniu.ingest.identity;
|
||||
|
||||
import com.lingniu.ingest.api.ProtocolId;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.time.Duration;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class MySqlVehicleIdentityService implements VehicleIdentityResolver, VehicleIdentityRegistry {
|
||||
public final class MySqlVehicleIdentityService implements VehicleIdentityResolver, VehicleIdentityRegistry, AutoCloseable {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(MySqlVehicleIdentityService.class);
|
||||
|
||||
private final DataSource dataSource;
|
||||
private final String table;
|
||||
private final InMemoryVehicleIdentityService index = new InMemoryVehicleIdentityService();
|
||||
private final ScheduledExecutorService refresher;
|
||||
private volatile InMemoryVehicleIdentityService index;
|
||||
|
||||
public MySqlVehicleIdentityService(DataSource dataSource, String table) {
|
||||
this(dataSource, table, Duration.ZERO);
|
||||
}
|
||||
|
||||
public MySqlVehicleIdentityService(DataSource dataSource, String table, Duration refreshInterval) {
|
||||
if (dataSource == null) {
|
||||
throw new IllegalArgumentException("dataSource must not be null");
|
||||
}
|
||||
this.dataSource = dataSource;
|
||||
this.table = sanitizeTable(table);
|
||||
initializeSchema();
|
||||
loadIndex();
|
||||
this.index = loadIndex();
|
||||
this.refresher = startRefresher(refreshInterval);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -45,6 +59,17 @@ public final class MySqlVehicleIdentityService implements VehicleIdentityResolve
|
||||
return index.resolve(lookup);
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
index = loadIndex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (refresher != null) {
|
||||
refresher.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeSchema() {
|
||||
String sql = """
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
@@ -66,24 +91,31 @@ public final class MySqlVehicleIdentityService implements VehicleIdentityResolve
|
||||
}
|
||||
}
|
||||
|
||||
private void loadIndex() {
|
||||
private InMemoryVehicleIdentityService loadIndex() {
|
||||
InMemoryVehicleIdentityService loaded = new InMemoryVehicleIdentityService();
|
||||
String sql = "SELECT protocol, identifier_type, identifier_value, vin FROM " + table;
|
||||
try (Connection connection = dataSource.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(sql);
|
||||
ResultSet resultSet = statement.executeQuery()) {
|
||||
while (resultSet.next()) {
|
||||
bindLoadedRow(
|
||||
loaded,
|
||||
protocol(resultSet.getString("protocol")),
|
||||
resultSet.getString("identifier_type"),
|
||||
resultSet.getString("identifier_value"),
|
||||
resultSet.getString("vin"));
|
||||
}
|
||||
return loaded;
|
||||
} catch (SQLException e) {
|
||||
throw new IllegalStateException("vehicle identity mysql index load failed: " + table, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void bindLoadedRow(ProtocolId protocol, String identifierType, String identifierValue, String vin) {
|
||||
private void bindLoadedRow(InMemoryVehicleIdentityService loaded,
|
||||
ProtocolId protocol,
|
||||
String identifierType,
|
||||
String identifierValue,
|
||||
String vin) {
|
||||
if (vin == null || vin.isBlank()) {
|
||||
return;
|
||||
}
|
||||
@@ -95,7 +127,29 @@ public final class MySqlVehicleIdentityService implements VehicleIdentityResolve
|
||||
default -> null;
|
||||
};
|
||||
if (binding != null) {
|
||||
index.bind(binding);
|
||||
loaded.bind(binding);
|
||||
}
|
||||
}
|
||||
|
||||
private ScheduledExecutorService startRefresher(Duration refreshInterval) {
|
||||
if (refreshInterval == null || refreshInterval.isZero() || refreshInterval.isNegative()) {
|
||||
return null;
|
||||
}
|
||||
long delayMillis = Math.max(refreshInterval.toMillis(), 1000L);
|
||||
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "vehicle-identity-mysql-refresh");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
executor.scheduleWithFixedDelay(this::safeRefresh, delayMillis, delayMillis, TimeUnit.MILLISECONDS);
|
||||
return executor;
|
||||
}
|
||||
|
||||
private void safeRefresh() {
|
||||
try {
|
||||
refresh();
|
||||
} catch (RuntimeException e) {
|
||||
log.warn("vehicle identity mysql refresh failed table={}", table, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,8 @@ public class VehicleIdentityAutoConfiguration {
|
||||
@ConditionalOnProperty(prefix = "lingniu.ingest.identity", name = "store", havingValue = "mysql")
|
||||
public MySqlVehicleIdentityService mySqlVehicleIdentityService(VehicleIdentityProperties properties,
|
||||
DataSource dataSource) {
|
||||
return new MySqlVehicleIdentityService(dataSource, properties.getMysql().getTable());
|
||||
VehicleIdentityProperties.Mysql mysql = properties.getMysql();
|
||||
return new MySqlVehicleIdentityService(dataSource, mysql.getTable(), mysql.getRefreshInterval());
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.lingniu.ingest.identity.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@ConfigurationProperties(prefix = "lingniu.ingest.identity")
|
||||
public class VehicleIdentityProperties {
|
||||
|
||||
@@ -29,6 +31,7 @@ public class VehicleIdentityProperties {
|
||||
private String username = "";
|
||||
private String password = "";
|
||||
private String driverClassName = "com.mysql.cj.jdbc.Driver";
|
||||
private Duration refreshInterval = Duration.ofSeconds(60);
|
||||
|
||||
public String getTable() { return table; }
|
||||
public void setTable(String table) { this.table = table; }
|
||||
@@ -40,5 +43,7 @@ public class VehicleIdentityProperties {
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
public String getDriverClassName() { return driverClassName; }
|
||||
public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; }
|
||||
public Duration getRefreshInterval() { return refreshInterval; }
|
||||
public void setRefreshInterval(Duration refreshInterval) { this.refreshInterval = refreshInterval; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,27 @@ class MySqlVehicleIdentityServiceTest {
|
||||
assertThat(jdbc.lookupSelects).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshLoadsExternallyInsertedBindingWithoutPerFrameDatabaseLookup() {
|
||||
RecordingIdentityJdbc jdbc = new RecordingIdentityJdbc();
|
||||
MySqlVehicleIdentityService service = new MySqlVehicleIdentityService(jdbc.dataSource(), "vehicle_identity_binding");
|
||||
|
||||
VehicleIdentity before = service.resolve(new VehicleIdentityLookup(
|
||||
ProtocolId.JT808, "", "13079960002", "", ""));
|
||||
|
||||
jdbc.seed("JT808", "PHONE", "13079960002", "LNVIN000000000456");
|
||||
service.refresh();
|
||||
VehicleIdentity after = service.resolve(new VehicleIdentityLookup(
|
||||
ProtocolId.JT808, "", "13079960002", "", ""));
|
||||
|
||||
assertThat(before.resolved()).isFalse();
|
||||
assertThat(after.vin()).isEqualTo("LNVIN000000000456");
|
||||
assertThat(after.resolved()).isTrue();
|
||||
assertThat(after.source()).isEqualTo(VehicleIdentitySource.BOUND_PHONE);
|
||||
assertThat(jdbc.loadAllSelects).isEqualTo(2);
|
||||
assertThat(jdbc.lookupSelects).isZero();
|
||||
}
|
||||
|
||||
private static final class RecordingIdentityJdbc {
|
||||
private final List<String> createdTables = new ArrayList<>();
|
||||
private final Map<String, String> bindings = new HashMap<>();
|
||||
|
||||
Reference in New Issue
Block a user