SHA256
245 lines
11 KiB
Java
245 lines
11 KiB
Java
package server.sync;
|
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import server.logic.ws_protocol.JSON.entyties.Net_Exception_Response;
|
|
import server.logic.ws_protocol.JSON.entyties.Net_Response;
|
|
import server.logic.ws_protocol.JSON.handlers.auth.SolanaUserPdaImportService;
|
|
import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler;
|
|
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request;
|
|
import shine.db.dao.BlockchainStateDAO;
|
|
import shine.db.dao.SyncServersDAO;
|
|
import shine.db.entities.BlockchainStateEntry;
|
|
import shine.db.entities.SyncServerEntry;
|
|
import utils.blockchain.BlockchainNameUtil;
|
|
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
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;
|
|
|
|
/**
|
|
* Плановый межсерверный sync блокчейнов.
|
|
* Сейчас реализует только догоняющую синхронизацию отсутствующего хвоста.
|
|
* Случай рассинхрона цепочек пока только логируется и пропускается.
|
|
*/
|
|
public final class PeriodicBlockchainSyncService {
|
|
|
|
private static final Logger log = LoggerFactory.getLogger(PeriodicBlockchainSyncService.class);
|
|
private static final long PERIOD_HOURS = 12L;
|
|
private static final AtomicBoolean STARTED = new AtomicBoolean(false);
|
|
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
|
|
@Override
|
|
public Thread newThread(Runnable r) {
|
|
Thread t = new Thread(r, "periodic-blockchain-sync");
|
|
t.setDaemon(true);
|
|
return t;
|
|
}
|
|
});
|
|
|
|
private static final RemoteBlockchainSyncClient REMOTE = new RemoteBlockchainSyncClient();
|
|
private static final Net_AddBlock_Handler ADD_BLOCK_HANDLER = new Net_AddBlock_Handler();
|
|
private static final BlockchainStateDAO STATE_DAO = BlockchainStateDAO.getInstance();
|
|
private static final SyncServersDAO SYNC_SERVERS_DAO = SyncServersDAO.getInstance();
|
|
|
|
private PeriodicBlockchainSyncService() {}
|
|
|
|
public static void startOrLog() {
|
|
if (!STARTED.compareAndSet(false, true)) {
|
|
return;
|
|
}
|
|
EXECUTOR.scheduleWithFixedDelay(
|
|
PeriodicBlockchainSyncService::runCycleSafe,
|
|
0L,
|
|
PERIOD_HOURS,
|
|
TimeUnit.HOURS
|
|
);
|
|
log.info("Periodic blockchain sync scheduled: startup + every {} hours", PERIOD_HOURS);
|
|
}
|
|
|
|
private static void runCycleSafe() {
|
|
try {
|
|
runCycle();
|
|
} catch (Exception e) {
|
|
log.error("Periodic blockchain sync failed unexpectedly", e);
|
|
}
|
|
}
|
|
|
|
private static void runCycle() throws Exception {
|
|
SyncServersBootstrapService.refreshFromSolanaOrLog();
|
|
List<SyncServerEntry> partners = SYNC_SERVERS_DAO.listAll();
|
|
if (partners.isEmpty()) {
|
|
log.info("Periodic blockchain sync skipped: sync_servers is empty");
|
|
return;
|
|
}
|
|
|
|
for (SyncServerEntry partner : partners) {
|
|
if (partner == null) continue;
|
|
try {
|
|
syncPartner(partner);
|
|
} catch (Exception e) {
|
|
log.warn("Periodic blockchain sync partner failed: login={} reason={}",
|
|
partner.getLogin(), String.valueOf(e));
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void syncPartner(SyncServerEntry partner) throws Exception {
|
|
String partnerLogin = normalize(partner.getLogin());
|
|
if (partnerLogin == null) return;
|
|
|
|
List<RemoteBlockchainSyncClient.RemoteBlockchainHead> remoteHeads =
|
|
REMOTE.listBlockchainHeads(partner.getServerAddress());
|
|
|
|
for (RemoteBlockchainSyncClient.RemoteBlockchainHead remoteHead : remoteHeads) {
|
|
if (remoteHead == null || remoteHead.blockchainName() == null || remoteHead.blockchainName().isBlank()) {
|
|
continue;
|
|
}
|
|
|
|
BlockchainStateEntry localState = STATE_DAO.getByBlockchainName(remoteHead.blockchainName());
|
|
if (localState == null) {
|
|
syncMissingTail(partner, remoteHead, -1, "");
|
|
continue;
|
|
}
|
|
|
|
int localLast = localState.getLastBlockNumber();
|
|
String localHash = toHex32(localState.getLastBlockHash());
|
|
|
|
if (localLast < remoteHead.lastBlockNumber()) {
|
|
syncMissingTail(partner, remoteHead, localLast, localHash);
|
|
continue;
|
|
}
|
|
|
|
if (localLast == remoteHead.lastBlockNumber()) {
|
|
if (localHash.equalsIgnoreCase(remoteHead.lastBlockHash())) {
|
|
continue;
|
|
}
|
|
log.warn("Periodic blockchain sync: divergence detected but not implemented yet. partner={} blockchainName={} localLast={} localHash={} remoteHash={} localSize={} remoteSize={}",
|
|
partnerLogin,
|
|
remoteHead.blockchainName(),
|
|
localLast,
|
|
localHash,
|
|
remoteHead.lastBlockHash(),
|
|
localState.getFileSizeBytes(),
|
|
remoteHead.fileSizeBytes());
|
|
continue;
|
|
}
|
|
|
|
log.info("Periodic blockchain sync skipped: local chain is not weaker. partner={} blockchainName={} localLast={} remoteLast={}",
|
|
partnerLogin, remoteHead.blockchainName(), localLast, remoteHead.lastBlockNumber());
|
|
}
|
|
}
|
|
|
|
private static void syncMissingTail(
|
|
SyncServerEntry partner,
|
|
RemoteBlockchainSyncClient.RemoteBlockchainHead remoteHead,
|
|
int localLast,
|
|
String localHash
|
|
) throws Exception {
|
|
String partnerLogin = normalize(partner.getLogin());
|
|
if (!ensureLocalChainExists(remoteHead.blockchainName())) {
|
|
log.warn("Periodic blockchain sync: cannot prepare local chain. partner={} blockchainName={}",
|
|
partnerLogin, remoteHead.blockchainName());
|
|
return;
|
|
}
|
|
|
|
int fromBlockNumber = Math.max(localLast + 1, 0);
|
|
for (int blockNumber = fromBlockNumber; blockNumber <= remoteHead.lastBlockNumber(); blockNumber++) {
|
|
RemoteBlockchainSyncClient.RemoteBlockchainBlock remoteBlock =
|
|
REMOTE.getBlockchainBlock(partner.getServerAddress(), remoteHead.blockchainName(), blockNumber);
|
|
if (remoteBlock == null) {
|
|
log.warn("Periodic blockchain sync: remote block not found. partner={} blockchainName={} blockNumber={}",
|
|
partnerLogin, remoteHead.blockchainName(), blockNumber);
|
|
return;
|
|
}
|
|
|
|
LocalAddBlockApplyResult result = applyBlockLocally(remoteBlock, blockNumber == 0 ? "" : localHash);
|
|
if (!result.ok()) {
|
|
if ("bad_prev_hash".equalsIgnoreCase(result.code()) || "bad_block_number".equalsIgnoreCase(result.code())) {
|
|
log.warn("Periodic blockchain sync: divergence detected during replay, but reconciliation is not implemented yet. partner={} blockchainName={} blockNumber={} code={}",
|
|
partnerLogin, remoteHead.blockchainName(), blockNumber, result.code());
|
|
} else {
|
|
log.warn("Periodic blockchain sync: local AddBlock rejected remote block. partner={} blockchainName={} blockNumber={} code={} message={}",
|
|
partnerLogin, remoteHead.blockchainName(), blockNumber, result.code(), result.message());
|
|
}
|
|
return;
|
|
}
|
|
|
|
localHash = result.serverLastHash();
|
|
}
|
|
|
|
log.info("Periodic blockchain sync ok: partner={} blockchainName={} from={} to={}",
|
|
partnerLogin, remoteHead.blockchainName(), fromBlockNumber, remoteHead.lastBlockNumber());
|
|
}
|
|
|
|
private static LocalAddBlockApplyResult applyBlockLocally(
|
|
RemoteBlockchainSyncClient.RemoteBlockchainBlock remoteBlock,
|
|
String prevHash
|
|
) {
|
|
Net_AddBlock_Request req = new Net_AddBlock_Request();
|
|
req.setOp("AddBlock");
|
|
req.setRequestId("periodic-sync-local");
|
|
req.setBlockchainName(remoteBlock.blockchainName());
|
|
req.setBlockNumber(remoteBlock.blockNumber());
|
|
req.setPrevBlockHash(prevHash == null ? "" : prevHash);
|
|
req.setBlockBytesB64(remoteBlock.blockBytesB64());
|
|
|
|
Net_Response response = ADD_BLOCK_HANDLER.handle(req, null);
|
|
if (response.getStatus() >= 200 && response.getStatus() < 300) {
|
|
server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Response ok =
|
|
(server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Response) response;
|
|
return new LocalAddBlockApplyResult(true, "", "", ok.getServerLastGlobalHash());
|
|
}
|
|
|
|
Net_Exception_Response error = (Net_Exception_Response) response;
|
|
return new LocalAddBlockApplyResult(false, error.getCode(), error.getMessage(), "");
|
|
}
|
|
|
|
private static boolean ensureLocalChainExists(String blockchainName) {
|
|
try {
|
|
if (STATE_DAO.getByBlockchainName(blockchainName) != null) {
|
|
return true;
|
|
}
|
|
String login = BlockchainNameUtil.loginFromBlockchainName(blockchainName);
|
|
if (login == null || login.isBlank()) {
|
|
return false;
|
|
}
|
|
SolanaUserPdaImportService.findOrImportByLogin(login);
|
|
return STATE_DAO.getByBlockchainName(blockchainName) != null;
|
|
} catch (Exception e) {
|
|
log.warn("Periodic blockchain sync: failed to ensure local chain exists for blockchainName={} reason={}",
|
|
blockchainName, String.valueOf(e));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static String normalize(String value) {
|
|
if (value == null) return null;
|
|
String s = value.trim().toLowerCase(Locale.ROOT);
|
|
return s.isEmpty() ? null : s;
|
|
}
|
|
|
|
private static String toHex32(byte[] bytes32) {
|
|
byte[] bytes = bytes32;
|
|
if (bytes == null || bytes.length != 32) {
|
|
bytes = new byte[32];
|
|
}
|
|
StringBuilder sb = new StringBuilder(64);
|
|
for (byte b : bytes) {
|
|
sb.append(Character.forDigit((b >>> 4) & 0xF, 16));
|
|
sb.append(Character.forDigit(b & 0xF, 16));
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
private record LocalAddBlockApplyResult(
|
|
boolean ok,
|
|
String code,
|
|
String message,
|
|
String serverLastHash
|
|
) {}
|
|
}
|