SHA256
257 lines
11 KiB
Java
257 lines
11 KiB
Java
package server.sync;
|
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import server.logic.ws_protocol.JSON.messages.DmSyncApplySupport;
|
|
import shine.db.dao.UserAccessServersCurrentDAO;
|
|
import shine.db.dao.UserSettingsDAO;
|
|
import shine.db.dao.UserSettingsSyncPeerStateDAO;
|
|
import shine.db.entities.UserAccessServerRouteEntry;
|
|
import shine.db.entities.UserSettingEntry;
|
|
import shine.db.entities.UserSettingsSyncPeerStateEntry;
|
|
import utils.config.AppConfig;
|
|
|
|
import java.sql.Connection;
|
|
import java.util.LinkedHashSet;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.ArrayList;
|
|
import java.util.Set;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.ScheduledExecutorService;
|
|
import java.util.concurrent.ThreadFactory;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
|
public final class PeriodicUserSettingsSyncService {
|
|
private static final Logger log = LoggerFactory.getLogger(PeriodicUserSettingsSyncService.class);
|
|
private static final String SERVER_LOGIN_CONFIG = "server.SHiNE.login";
|
|
private static final AtomicBoolean STARTED = new AtomicBoolean(false);
|
|
private static final RemoteUserSettingsSyncClient REMOTE = new RemoteUserSettingsSyncClient();
|
|
private static final RemoteDmSyncClient DM_REMOTE = new RemoteDmSyncClient();
|
|
private static final UserAccessServersCurrentDAO ACCESS_DAO = UserAccessServersCurrentDAO.getInstance();
|
|
private static final UserSettingsDAO SETTINGS_DAO = UserSettingsDAO.getInstance();
|
|
private static final UserSettingsSyncPeerStateDAO STATE_DAO = UserSettingsSyncPeerStateDAO.getInstance();
|
|
|
|
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
|
|
@Override
|
|
public Thread newThread(Runnable r) {
|
|
Thread t = new Thread(r, "periodic-user-settings-sync");
|
|
t.setDaemon(true);
|
|
return t;
|
|
}
|
|
});
|
|
|
|
private PeriodicUserSettingsSyncService() {}
|
|
|
|
public static void startOrLog() {
|
|
if (!isEnabled()) {
|
|
log.info("Periodic user settings sync disabled by user.settings.sync.enabled=false");
|
|
return;
|
|
}
|
|
if (!STARTED.compareAndSet(false, true)) return;
|
|
long initialDelaySec = configLong("user.settings.sync.initialDelaySeconds", 90L, 0L, 3600L);
|
|
long periodHours = configLong("user.settings.sync.periodHours", 6L, 1L, 168L);
|
|
EXECUTOR.scheduleWithFixedDelay(
|
|
PeriodicUserSettingsSyncService::runCycleSafe,
|
|
initialDelaySec,
|
|
TimeUnit.HOURS.toSeconds(periodHours),
|
|
TimeUnit.SECONDS
|
|
);
|
|
EXECUTOR.scheduleWithFixedDelay(
|
|
PeriodicUserSettingsSyncService::runRequestedCycleSafe,
|
|
5L, 5L, TimeUnit.SECONDS);
|
|
log.info("Periodic user settings sync scheduled: first run in {} seconds, then every {} hours", initialDelaySec, periodHours);
|
|
}
|
|
|
|
private static void runCycleSafe() {
|
|
try {
|
|
runCycle();
|
|
} catch (Exception e) {
|
|
log.error("Periodic user settings sync failed unexpectedly", e);
|
|
}
|
|
}
|
|
|
|
private static void runRequestedCycleSafe() {
|
|
if (DmSyncWakeSignal.consume()) runCycleSafe();
|
|
}
|
|
|
|
private static void runCycle() throws Exception {
|
|
String ownServerLogin = normalize(AppConfig.getInstance().getParam(SERVER_LOGIN_CONFIG));
|
|
if (ownServerLogin == null) {
|
|
log.warn("Periodic user settings sync skipped: {} is empty", SERVER_LOGIN_CONFIG);
|
|
return;
|
|
}
|
|
|
|
List<String> ownersRaw = ACCESS_DAO.listUserLoginsByServerLogin(ownServerLogin);
|
|
Set<String> owners = new LinkedHashSet<>(ownersRaw);
|
|
if (owners.isEmpty()) {
|
|
log.info("Periodic user settings sync skipped: no local access-server users for {}", ownServerLogin);
|
|
return;
|
|
}
|
|
|
|
int syncedPeers = 0;
|
|
int appliedItems = 0;
|
|
int pushedItems = 0;
|
|
int appliedDmItems = 0;
|
|
for (String ownerLogin : owners) {
|
|
List<UserAccessServerRouteEntry> routes = ACCESS_DAO.listByUserLogin(ownerLogin);
|
|
for (UserAccessServerRouteEntry route : routes) {
|
|
if (route == null) continue;
|
|
String remoteLogin = normalize(route.getServerLogin());
|
|
String remoteUrl = route.getServerUrl();
|
|
if (remoteLogin == null || remoteUrl == null || remoteUrl.isBlank()) continue;
|
|
if (remoteLogin.equals(ownServerLogin)) continue;
|
|
|
|
try {
|
|
SyncStats stats = syncOwnerWithRemote(ownerLogin, route);
|
|
appliedItems += stats.applied();
|
|
pushedItems += stats.pushed();
|
|
appliedDmItems += stats.appliedDm();
|
|
syncedPeers++;
|
|
} catch (Exception e) {
|
|
STATE_DAO.updateError(ownerLogin, route.getServerLogin(), remoteUrl, String.valueOf(e));
|
|
log.warn("Periodic user settings sync peer failed: owner={} remoteServer={} reason={}",
|
|
ownerLogin, route.getServerLogin(), String.valueOf(e));
|
|
}
|
|
}
|
|
}
|
|
|
|
log.info("Periodic access-data sync finished: owners={} peers={} settingsApplied={} settingsPushed={} dmApplied={}",
|
|
owners.size(), syncedPeers, appliedItems, pushedItems, appliedDmItems);
|
|
}
|
|
|
|
private static SyncStats syncOwnerWithRemote(String ownerLogin, UserAccessServerRouteEntry route) throws Exception {
|
|
int limit = (int) configLong("user.settings.sync.batchLimit", 500L, 1L, 1000L);
|
|
int maxBytes = (int) configLong("user.settings.sync.batchMaxBytes", 3_000_000L, 64_000L, 5_000_000L);
|
|
int maxPages = (int) configLong("user.settings.sync.maxPagesPerPeer", 50L, 1L, 500L);
|
|
|
|
UserSettingsSyncPeerStateEntry state;
|
|
try (Connection c = getDbConnection()) {
|
|
state = STATE_DAO.getOrCreate(c, ownerLogin, route.getServerLogin(), route.getServerUrl());
|
|
}
|
|
long cursorTimeMs = state.getCursorTimeMs();
|
|
String cursorSettingKey = state.getCursorSettingKey() == null ? "" : state.getCursorSettingKey();
|
|
int applied = 0;
|
|
int pushed = 0;
|
|
boolean bootstrapCompleted = false;
|
|
|
|
int appliedDm;
|
|
try (RemoteSyncSession session = new RemoteSyncSession(route.getServerUrl())) {
|
|
for (int page = 0; page < maxPages; page++) {
|
|
RemoteUserSettingsSyncClient.RemoteUserSettingsBatch batch = REMOTE.userSettingsSyncBatch(
|
|
session,
|
|
ownerLogin,
|
|
cursorTimeMs,
|
|
cursorSettingKey,
|
|
limit,
|
|
maxBytes
|
|
);
|
|
|
|
try (Connection c = getDbConnection()) {
|
|
for (RemoteUserSettingsSyncClient.RemoteUserSettingsItem item : batch.items()) {
|
|
UserSettingEntry entry = new UserSettingEntry(
|
|
item.login(),
|
|
item.settingType(),
|
|
item.settingKey(),
|
|
item.timeMs(),
|
|
item.valueText(),
|
|
item.valueNum(),
|
|
item.clientKey(),
|
|
item.signature(),
|
|
true
|
|
);
|
|
int changed = SETTINGS_DAO.upsertIfNewer(c, entry);
|
|
if (changed > 0) applied++;
|
|
}
|
|
}
|
|
|
|
cursorTimeMs = Math.max(cursorTimeMs, batch.nextTimeMs());
|
|
cursorSettingKey = batch.nextSettingKey() == null ? "" : batch.nextSettingKey();
|
|
bootstrapCompleted = !batch.hasMore();
|
|
STATE_DAO.updateSuccess(ownerLogin, route.getServerLogin(), route.getServerUrl(), cursorTimeMs, cursorSettingKey, bootstrapCompleted);
|
|
|
|
if (!batch.hasMore() || batch.items().isEmpty()) break;
|
|
}
|
|
|
|
try (Connection c = getDbConnection()) {
|
|
List<UserSettingEntry> unsynced = SETTINGS_DAO.listUnsyncedByLogin(c, ownerLogin, limit);
|
|
for (UserSettingEntry entry : unsynced) {
|
|
REMOTE.upsertUserSetting(session, entry, true);
|
|
SETTINGS_DAO.markSynced(c, entry.getLogin(), entry.getSettingType(), entry.getSettingKey());
|
|
pushed++;
|
|
}
|
|
}
|
|
|
|
int dmLimit = (int) configLong("dm.sync.batchLimit", 200L, 1L, 500L);
|
|
int dmMaxBytes = (int) configLong("dm.sync.batchMaxBytes", 3_000_000L, 64_000L, 5_000_000L);
|
|
int dmMaxPages = (int) configLong("dm.sync.maxPagesPerPeer", 20L, 1L, 500L);
|
|
appliedDm = syncDmInSameSession(session, ownerLogin, dmLimit, dmMaxBytes, dmMaxPages);
|
|
}
|
|
|
|
if (!bootstrapCompleted) {
|
|
log.info("Periodic user settings sync peer paused by page limit: owner={} remoteServer={} maxPages={}",
|
|
ownerLogin, route.getServerLogin(), maxPages);
|
|
}
|
|
return new SyncStats(applied, pushed, appliedDm);
|
|
}
|
|
|
|
private static int syncDmInSameSession(
|
|
RemoteSyncSession session, String ownerLogin, int limit, int maxBytes, int maxPages
|
|
) throws Exception {
|
|
long cursorMs = 0L;
|
|
String cursorKey = "";
|
|
List<String> acknowledgements = new ArrayList<>();
|
|
int applied = 0;
|
|
for (int page = 0; page < maxPages; page++) {
|
|
RemoteDmSyncClient.RemoteDmBatch batch = DM_REMOTE.dmSyncBatch(
|
|
session, ownerLogin, cursorMs, cursorKey, Math.min(limit, 500), maxBytes, acknowledgements);
|
|
acknowledgements = new ArrayList<>();
|
|
for (RemoteDmSyncClient.RemoteDmItem item : batch.items()) {
|
|
if (item == null || item.syncId() == null || item.syncId().isBlank()) continue;
|
|
DmSyncApplySupport.applySyncedItem(ownerLogin, item.syncId(), item.blobsB64());
|
|
acknowledgements.add(item.syncId());
|
|
applied++;
|
|
}
|
|
cursorMs = batch.nextStoredAtMs();
|
|
cursorKey = batch.nextMessageKey() == null ? "" : batch.nextMessageKey();
|
|
if (!batch.hasMore() || batch.items().isEmpty()) {
|
|
if (!acknowledgements.isEmpty()) {
|
|
DM_REMOTE.dmSyncBatch(session, ownerLogin, 0L, "",
|
|
Math.min(limit, 500), maxBytes, acknowledgements);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return applied;
|
|
}
|
|
|
|
private static java.sql.Connection getDbConnection() throws Exception {
|
|
return shine.db.DbController.getInstance().getConnection();
|
|
}
|
|
|
|
private static boolean isEnabled() {
|
|
String raw = AppConfig.getInstance().getParam("user.settings.sync.enabled");
|
|
return raw == null || raw.isBlank() || Boolean.parseBoolean(raw.trim());
|
|
}
|
|
|
|
private static long configLong(String key, long defaultValue, long min, long max) {
|
|
String raw = AppConfig.getInstance().getParam(key);
|
|
if (raw == null || raw.isBlank()) return defaultValue;
|
|
try {
|
|
long parsed = Long.parseLong(raw.trim());
|
|
return Math.max(min, Math.min(max, parsed));
|
|
} catch (Exception ignored) {
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
private static String normalize(String value) {
|
|
if (value == null) return null;
|
|
String s = value.trim().toLowerCase(Locale.ROOT);
|
|
return s.isEmpty() ? null : s;
|
|
}
|
|
|
|
private record SyncStats(int applied, int pushed, int appliedDm) {}
|
|
}
|