Files
SHiNE-server/SHiNE-server/src/main/java/server/sync/PeriodicBlockchainSyncService.java
T

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
) {}
}