SHA256
Сохранить текущий снимок DM-синхронизации
This commit is contained in:
@@ -28,6 +28,8 @@ public final class DatabaseInitializer {
|
||||
public static final int SCHEMA_VERSION_9 = 9;
|
||||
public static final int SCHEMA_VERSION_10 = 10;
|
||||
public static final int SCHEMA_VERSION_11 = 11;
|
||||
public static final int SCHEMA_VERSION_12 = 12;
|
||||
public static final int SCHEMA_VERSION_13 = 13;
|
||||
public static final String POSTGRES_SCHEMA_RESOURCE = "postgres/schema_v1.sql";
|
||||
public static final String POSTGRES_MIGRATION_V2_RESOURCE = "postgres/migration_v2.sql";
|
||||
public static final String POSTGRES_MIGRATION_V3_RESOURCE = "postgres/migration_v3.sql";
|
||||
@@ -39,6 +41,8 @@ public final class DatabaseInitializer {
|
||||
public static final String POSTGRES_MIGRATION_V9_RESOURCE = "postgres/migration_v9.sql";
|
||||
public static final String POSTGRES_MIGRATION_V10_RESOURCE = "postgres/migration_v10.sql";
|
||||
public static final String POSTGRES_MIGRATION_V11_RESOURCE = "postgres/migration_v11.sql";
|
||||
public static final String POSTGRES_MIGRATION_V12_RESOURCE = "postgres/migration_v12.sql";
|
||||
public static final String POSTGRES_MIGRATION_V13_RESOURCE = "postgres/migration_v13.sql";
|
||||
|
||||
private DatabaseInitializer() {}
|
||||
|
||||
@@ -142,6 +146,14 @@ public final class DatabaseInitializer {
|
||||
runSqlScript(conn, POSTGRES_MIGRATION_V11_RESOURCE);
|
||||
currentVersion = SCHEMA_VERSION_11;
|
||||
}
|
||||
if (currentVersion < SCHEMA_VERSION_12) {
|
||||
runSqlScript(conn, POSTGRES_MIGRATION_V12_RESOURCE);
|
||||
currentVersion = SCHEMA_VERSION_12;
|
||||
}
|
||||
if (currentVersion < SCHEMA_VERSION_13) {
|
||||
runSqlScript(conn, POSTGRES_MIGRATION_V13_RESOURCE);
|
||||
currentVersion = SCHEMA_VERSION_13;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,518 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.DbController;
|
||||
import shine.db.entities.DmDeliveryStateEntry;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** Хранилище изменяемого состояния межсерверной доставки DM-пары. */
|
||||
public final class DmDeliveryStateDAO {
|
||||
private static volatile DmDeliveryStateDAO instance;
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private DmDeliveryStateDAO() {}
|
||||
|
||||
public static DmDeliveryStateDAO getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (DmDeliveryStateDAO.class) {
|
||||
if (instance == null) instance = new DmDeliveryStateDAO();
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public DmDeliveryStateEntry upsertPair(
|
||||
String outgoingMessageKey,
|
||||
String eventId,
|
||||
String baseKey,
|
||||
String fromLogin,
|
||||
String toLogin,
|
||||
String incomingMessageKey,
|
||||
long createdAtMs,
|
||||
long expiresAtMs,
|
||||
int initialState,
|
||||
String deliveredServerLogin,
|
||||
String routesHash,
|
||||
boolean assistImmediately
|
||||
) throws SQLException {
|
||||
long now = System.currentTimeMillis();
|
||||
int safeState = normalizeState(initialState, deliveredServerLogin);
|
||||
if (expiresAtMs <= now && safeState == DmDeliveryStateEntry.PENDING_NONE) {
|
||||
safeState = DmDeliveryStateEntry.FAILED_FINAL;
|
||||
}
|
||||
Long nextAttemptAt = safeState == DmDeliveryStateEntry.ACCEPTED
|
||||
? now
|
||||
: null;
|
||||
String safeDeliveredLogin = safeState == DmDeliveryStateEntry.DELIVERED_ONE
|
||||
? normalize(deliveredServerLogin)
|
||||
: null;
|
||||
|
||||
try (Connection c = db.getConnection()) {
|
||||
String sql = """
|
||||
INSERT INTO dm_delivery_state (
|
||||
outgoing_message_key, event_id, base_key, from_login, to_login,
|
||||
incoming_message_key, created_at_ms, delivery_expires_at_ms,
|
||||
delivery_state, delivered_server_login, recipient_routes_hash,
|
||||
attempt_index, next_attempt_at_ms, last_attempt_at_ms, last_error, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, NULL, NULL, ?)
|
||||
ON CONFLICT (outgoing_message_key) DO UPDATE SET
|
||||
event_id = EXCLUDED.event_id,
|
||||
base_key = EXCLUDED.base_key,
|
||||
from_login = EXCLUDED.from_login,
|
||||
to_login = EXCLUDED.to_login,
|
||||
incoming_message_key = EXCLUDED.incoming_message_key,
|
||||
created_at_ms = CASE
|
||||
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.created_at_ms
|
||||
ELSE EXCLUDED.created_at_ms
|
||||
END,
|
||||
delivery_expires_at_ms = CASE
|
||||
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.delivery_expires_at_ms
|
||||
ELSE EXCLUDED.delivery_expires_at_ms
|
||||
END,
|
||||
delivery_state = CASE
|
||||
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.delivery_state
|
||||
ELSE EXCLUDED.delivery_state
|
||||
END,
|
||||
delivered_server_login = CASE
|
||||
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.delivered_server_login
|
||||
ELSE EXCLUDED.delivered_server_login
|
||||
END,
|
||||
recipient_routes_hash = CASE
|
||||
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN COALESCE(dm_delivery_state.recipient_routes_hash, EXCLUDED.recipient_routes_hash)
|
||||
ELSE EXCLUDED.recipient_routes_hash
|
||||
END,
|
||||
attempt_index = CASE
|
||||
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.attempt_index
|
||||
ELSE 0
|
||||
END,
|
||||
next_attempt_at_ms = CASE
|
||||
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.next_attempt_at_ms
|
||||
ELSE EXCLUDED.next_attempt_at_ms
|
||||
END,
|
||||
last_attempt_at_ms = CASE
|
||||
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.last_attempt_at_ms
|
||||
ELSE NULL
|
||||
END,
|
||||
last_error = CASE
|
||||
WHEN dm_delivery_state.event_id = EXCLUDED.event_id THEN dm_delivery_state.last_error
|
||||
ELSE NULL
|
||||
END,
|
||||
updated_at_ms = EXCLUDED.updated_at_ms
|
||||
""";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, outgoingMessageKey);
|
||||
ps.setString(2, eventId);
|
||||
ps.setString(3, baseKey);
|
||||
ps.setString(4, normalize(fromLogin));
|
||||
ps.setString(5, normalize(toLogin));
|
||||
ps.setString(6, incomingMessageKey);
|
||||
ps.setLong(7, createdAtMs);
|
||||
ps.setLong(8, expiresAtMs);
|
||||
ps.setInt(9, safeState);
|
||||
setNullableString(ps, 10, safeDeliveredLogin);
|
||||
setNullableString(ps, 11, normalizeBlank(routesHash));
|
||||
setNullableLong(ps, 12, nextAttemptAt);
|
||||
ps.setLong(13, now);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
DmDeliveryStateEntry result;
|
||||
if (initialState != DmDeliveryStateEntry.PENDING_NONE || !isBlank(deliveredServerLogin)) {
|
||||
result = mergeRemote(eventId, initialState, deliveredServerLogin, routesHash, now);
|
||||
} else {
|
||||
result = getByEventId(eventId);
|
||||
}
|
||||
if (result != null && result.getDeliveryExpiresAtMs() <= now
|
||||
&& result.getDeliveryState() == DmDeliveryStateEntry.PENDING_NONE) {
|
||||
return finishAtExpiry(eventId, now);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public DmDeliveryStateEntry getByEventId(String eventId) throws SQLException {
|
||||
if (isBlank(eventId)) return null;
|
||||
try (Connection c = db.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement(selectColumns() + " WHERE event_id = ?")) {
|
||||
ps.setString(1, eventId.trim());
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
return rs.next() ? mapRow(rs) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DmDeliveryStateEntry getByOutgoingMessageKey(String outgoingMessageKey) throws SQLException {
|
||||
if (isBlank(outgoingMessageKey)) return null;
|
||||
try (Connection c = db.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement(selectColumns() + " WHERE outgoing_message_key = ?")) {
|
||||
ps.setString(1, outgoingMessageKey.trim());
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
return rs.next() ? mapRow(rs) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, DmDeliveryStateEntry> listByOutgoingMessageKeys(List<String> keys) throws SQLException {
|
||||
Map<String, DmDeliveryStateEntry> out = new HashMap<>();
|
||||
if (keys == null || keys.isEmpty()) return out;
|
||||
String placeholders = String.join(",", java.util.Collections.nCopies(keys.size(), "?"));
|
||||
String sql = selectColumns() + " WHERE outgoing_message_key IN (" + placeholders + ")";
|
||||
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
for (int i = 0; i < keys.size(); i++) ps.setString(i + 1, keys.get(i));
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
DmDeliveryStateEntry row = mapRow(rs);
|
||||
out.put(row.getOutgoingMessageKey(), row);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public List<DmDeliveryStateEntry> listDue(long nowMs, int limit) throws SQLException {
|
||||
String sql = selectColumns() + """
|
||||
WHERE delivery_state = 0
|
||||
AND next_attempt_at_ms IS NOT NULL
|
||||
AND next_attempt_at_ms <= ?
|
||||
ORDER BY next_attempt_at_ms ASC, created_at_ms ASC
|
||||
LIMIT ?
|
||||
""";
|
||||
List<DmDeliveryStateEntry> out = new ArrayList<>();
|
||||
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setLong(1, nowMs);
|
||||
ps.setInt(2, Math.max(1, limit));
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
while (rs.next()) out.add(mapRow(rs));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public boolean claimAttempt(DmDeliveryStateEntry row, long nowMs, Long nextAttemptAtMs) throws SQLException {
|
||||
String sql = """
|
||||
UPDATE dm_delivery_state
|
||||
SET attempt_index = attempt_index + 1,
|
||||
next_attempt_at_ms = ?,
|
||||
last_attempt_at_ms = ?,
|
||||
last_error = NULL,
|
||||
updated_at_ms = ?
|
||||
WHERE outgoing_message_key = ?
|
||||
AND event_id = ?
|
||||
AND attempt_index = ?
|
||||
AND delivery_state = 0
|
||||
AND next_attempt_at_ms IS NOT NULL
|
||||
AND next_attempt_at_ms <= ?
|
||||
""";
|
||||
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
long networkLeaseUntilMs = nowMs + 60_000L;
|
||||
Long claimedNextAttemptAtMs = nextAttemptAtMs == null
|
||||
? networkLeaseUntilMs
|
||||
: Math.max(nextAttemptAtMs, networkLeaseUntilMs);
|
||||
setNullableLong(ps, 1, claimedNextAttemptAtMs);
|
||||
ps.setLong(2, nowMs);
|
||||
ps.setLong(3, nowMs);
|
||||
ps.setString(4, row.getOutgoingMessageKey());
|
||||
ps.setString(5, row.getEventId());
|
||||
ps.setInt(6, row.getAttemptIndex());
|
||||
ps.setLong(7, nowMs);
|
||||
return ps.executeUpdate() == 1;
|
||||
}
|
||||
}
|
||||
|
||||
public DmDeliveryStateEntry mergeRemote(
|
||||
String eventId,
|
||||
int remoteState,
|
||||
String remoteDeliveredLogin,
|
||||
String remoteRoutesHash,
|
||||
long nowMs
|
||||
) throws SQLException {
|
||||
try (Connection c = db.getConnection()) {
|
||||
boolean previousAutoCommit = c.getAutoCommit();
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
DmDeliveryStateEntry current = getByEventIdForUpdate(c, eventId);
|
||||
if (current == null) {
|
||||
c.rollback();
|
||||
return null;
|
||||
}
|
||||
MergeResult merged = merge(
|
||||
current.getDeliveryState(),
|
||||
current.getDeliveredServerLogin(),
|
||||
current.getRecipientRoutesHash(),
|
||||
remoteState,
|
||||
remoteDeliveredLogin,
|
||||
remoteRoutesHash
|
||||
);
|
||||
updateMutableState(c, current, merged.state(), merged.deliveredLogin(),
|
||||
merged.routesHash(), current.getNextAttemptAtMs(), current.getLastError(), nowMs);
|
||||
c.commit();
|
||||
return getByEventId(eventId);
|
||||
} catch (Exception e) {
|
||||
try { c.rollback(); } catch (Exception ignored) {}
|
||||
throw e;
|
||||
} finally {
|
||||
c.setAutoCommit(previousAutoCommit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DmDeliveryStateEntry updateAfterAttempt(
|
||||
String eventId,
|
||||
int attemptedState,
|
||||
String deliveredServerLogin,
|
||||
String routesHash,
|
||||
Long nextAttemptAtMs,
|
||||
String lastError,
|
||||
long nowMs
|
||||
) throws SQLException {
|
||||
try (Connection c = db.getConnection()) {
|
||||
boolean previousAutoCommit = c.getAutoCommit();
|
||||
c.setAutoCommit(false);
|
||||
try {
|
||||
DmDeliveryStateEntry current = getByEventIdForUpdate(c, eventId);
|
||||
if (current == null) {
|
||||
c.rollback();
|
||||
return null;
|
||||
}
|
||||
MergeResult merged = merge(
|
||||
current.getDeliveryState(), current.getDeliveredServerLogin(), current.getRecipientRoutesHash(),
|
||||
attemptedState, deliveredServerLogin, routesHash
|
||||
);
|
||||
Long effectiveNext = merged.state() == DmDeliveryStateEntry.ACCEPTED
|
||||
? nextAttemptAtMs
|
||||
: null;
|
||||
updateMutableState(c, current, merged.state(), merged.deliveredLogin(),
|
||||
merged.routesHash(), effectiveNext, lastError, nowMs);
|
||||
c.commit();
|
||||
return getByEventId(eventId);
|
||||
} catch (Exception e) {
|
||||
try { c.rollback(); } catch (Exception ignored) {}
|
||||
throw e;
|
||||
} finally {
|
||||
c.setAutoCommit(previousAutoCommit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DmDeliveryStateEntry finishAtExpiry(String eventId, long nowMs) throws SQLException {
|
||||
try (Connection c = db.getConnection()) {
|
||||
String sql = """
|
||||
UPDATE dm_delivery_state
|
||||
SET delivery_state = CASE WHEN delivery_state = 0 THEN 3 ELSE delivery_state END,
|
||||
delivered_server_login = CASE WHEN delivery_state = 0 THEN NULL ELSE delivered_server_login END,
|
||||
next_attempt_at_ms = NULL,
|
||||
last_error = CASE WHEN delivery_state = 0 THEN 'DELIVERY_EXPIRED' ELSE last_error END,
|
||||
updated_at_ms = ?
|
||||
WHERE event_id = ? AND delivery_state = 0
|
||||
""";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setLong(1, nowMs);
|
||||
ps.setString(2, eventId);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
return getByEventId(eventId);
|
||||
}
|
||||
|
||||
/** Read-only peer confirmed that at least one recipient server accepted the message. */
|
||||
public DmDeliveryStateEntry markDeliveredFromPeer(String eventId, long nowMs) throws SQLException {
|
||||
try (Connection c = db.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement("""
|
||||
UPDATE dm_delivery_state
|
||||
SET delivery_state = 1,
|
||||
delivered_server_login = NULL,
|
||||
next_attempt_at_ms = NULL,
|
||||
last_error = NULL,
|
||||
updated_at_ms = ?
|
||||
WHERE event_id = ? AND delivery_state = 0
|
||||
""")) {
|
||||
ps.setLong(1, nowMs);
|
||||
ps.setString(2, eventId);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
return getByEventId(eventId);
|
||||
}
|
||||
|
||||
public int removeByBaseKey(String baseKey) throws SQLException {
|
||||
try (Connection c = db.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement("DELETE FROM dm_delivery_state WHERE base_key = ?")) {
|
||||
ps.setString(1, baseKey);
|
||||
return ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public int removeConversationBefore(String fromLogin, String toLogin, long boundaryTimeMs) throws SQLException {
|
||||
String sql = """
|
||||
DELETE FROM dm_delivery_state
|
||||
WHERE created_at_ms < ?
|
||||
AND ((LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?))
|
||||
OR (LOWER(from_login) = LOWER(?) AND LOWER(to_login) = LOWER(?)))
|
||||
""";
|
||||
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setLong(1, boundaryTimeMs);
|
||||
ps.setString(2, fromLogin);
|
||||
ps.setString(3, toLogin);
|
||||
ps.setString(4, toLogin);
|
||||
ps.setString(5, fromLogin);
|
||||
return ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public int removeMissingMessages() throws SQLException {
|
||||
String sql = """
|
||||
DELETE FROM dm_delivery_state d
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM signed_messages m WHERE m.message_key = d.outgoing_message_key
|
||||
)
|
||||
""";
|
||||
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
return ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private DmDeliveryStateEntry getByEventIdForUpdate(Connection c, String eventId) throws SQLException {
|
||||
try (PreparedStatement ps = c.prepareStatement(selectColumns() + " WHERE event_id = ? FOR UPDATE")) {
|
||||
ps.setString(1, eventId);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
return rs.next() ? mapRow(rs) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMutableState(
|
||||
Connection c,
|
||||
DmDeliveryStateEntry current,
|
||||
int state,
|
||||
String deliveredLogin,
|
||||
String routesHash,
|
||||
Long nextAttemptAtMs,
|
||||
String lastError,
|
||||
long nowMs
|
||||
) throws SQLException {
|
||||
String sql = """
|
||||
UPDATE dm_delivery_state
|
||||
SET delivery_state = ?,
|
||||
delivered_server_login = ?,
|
||||
recipient_routes_hash = ?,
|
||||
next_attempt_at_ms = ?,
|
||||
last_error = ?,
|
||||
updated_at_ms = ?
|
||||
WHERE outgoing_message_key = ? AND event_id = ?
|
||||
""";
|
||||
try (PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setInt(1, state);
|
||||
setNullableString(ps, 2, state == DmDeliveryStateEntry.DELIVERED_ONE ? normalize(deliveredLogin) : null);
|
||||
setNullableString(ps, 3, normalizeBlank(routesHash));
|
||||
setNullableLong(ps, 4, nextAttemptAtMs);
|
||||
setNullableString(ps, 5, normalizeBlank(lastError));
|
||||
ps.setLong(6, nowMs);
|
||||
ps.setString(7, current.getOutgoingMessageKey());
|
||||
ps.setString(8, current.getEventId());
|
||||
ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private MergeResult merge(
|
||||
int localState,
|
||||
String localLogin,
|
||||
String localHash,
|
||||
int remoteStateRaw,
|
||||
String remoteLoginRaw,
|
||||
String remoteHashRaw
|
||||
) {
|
||||
int remoteState = normalizeState(remoteStateRaw, remoteLoginRaw);
|
||||
String remoteLogin = normalize(remoteLoginRaw);
|
||||
String localNormalizedLogin = normalize(localLogin);
|
||||
String localRoutesHash = normalizeBlank(localHash);
|
||||
String remoteRoutesHash = normalizeBlank(remoteHashRaw);
|
||||
String resultHash = remoteRoutesHash != null ? remoteRoutesHash : localRoutesHash;
|
||||
|
||||
if (localState == DmDeliveryStateEntry.DELIVERED_ONE || localState == DmDeliveryStateEntry.DELIVERED_ALL) {
|
||||
return new MergeResult(DmDeliveryStateEntry.DELIVERED, localNormalizedLogin,
|
||||
localRoutesHash != null ? localRoutesHash : remoteRoutesHash);
|
||||
}
|
||||
if (remoteState == DmDeliveryStateEntry.DELIVERED_ONE || remoteState == DmDeliveryStateEntry.DELIVERED_ALL) {
|
||||
return new MergeResult(DmDeliveryStateEntry.DELIVERED, remoteLogin, resultHash);
|
||||
}
|
||||
if (localState == DmDeliveryStateEntry.FAILED_FINAL || remoteState == DmDeliveryStateEntry.FAILED_FINAL) {
|
||||
return new MergeResult(DmDeliveryStateEntry.FAILED_FINAL, null, resultHash);
|
||||
}
|
||||
return new MergeResult(DmDeliveryStateEntry.PENDING_NONE, null, resultHash);
|
||||
}
|
||||
|
||||
private int normalizeState(int state, String deliveredLogin) {
|
||||
if (state < DmDeliveryStateEntry.PENDING_NONE || state > DmDeliveryStateEntry.FAILED_FINAL) {
|
||||
return DmDeliveryStateEntry.PENDING_NONE;
|
||||
}
|
||||
if (state == DmDeliveryStateEntry.DELIVERED_ALL) return DmDeliveryStateEntry.DELIVERED;
|
||||
return state;
|
||||
}
|
||||
|
||||
private String selectColumns() {
|
||||
return """
|
||||
SELECT outgoing_message_key, event_id, base_key, from_login, to_login,
|
||||
incoming_message_key, created_at_ms, delivery_expires_at_ms,
|
||||
delivery_state, delivered_server_login, recipient_routes_hash,
|
||||
attempt_index, next_attempt_at_ms, last_attempt_at_ms, last_error, updated_at_ms
|
||||
FROM dm_delivery_state
|
||||
""";
|
||||
}
|
||||
|
||||
private DmDeliveryStateEntry mapRow(ResultSet rs) throws SQLException {
|
||||
DmDeliveryStateEntry e = new DmDeliveryStateEntry();
|
||||
e.setOutgoingMessageKey(rs.getString("outgoing_message_key"));
|
||||
e.setEventId(rs.getString("event_id"));
|
||||
e.setBaseKey(rs.getString("base_key"));
|
||||
e.setFromLogin(rs.getString("from_login"));
|
||||
e.setToLogin(rs.getString("to_login"));
|
||||
e.setIncomingMessageKey(rs.getString("incoming_message_key"));
|
||||
e.setCreatedAtMs(rs.getLong("created_at_ms"));
|
||||
e.setDeliveryExpiresAtMs(rs.getLong("delivery_expires_at_ms"));
|
||||
e.setDeliveryState(rs.getInt("delivery_state"));
|
||||
e.setDeliveredServerLogin(rs.getString("delivered_server_login"));
|
||||
e.setRecipientRoutesHash(rs.getString("recipient_routes_hash"));
|
||||
e.setAttemptIndex(rs.getInt("attempt_index"));
|
||||
long next = rs.getLong("next_attempt_at_ms");
|
||||
e.setNextAttemptAtMs(rs.wasNull() ? null : next);
|
||||
long last = rs.getLong("last_attempt_at_ms");
|
||||
e.setLastAttemptAtMs(rs.wasNull() ? null : last);
|
||||
e.setLastError(rs.getString("last_error"));
|
||||
e.setUpdatedAtMs(rs.getLong("updated_at_ms"));
|
||||
return e;
|
||||
}
|
||||
|
||||
private static void setNullableString(PreparedStatement ps, int index, String value) throws SQLException {
|
||||
if (isBlank(value)) ps.setNull(index, Types.VARCHAR);
|
||||
else ps.setString(index, value.trim());
|
||||
}
|
||||
|
||||
private static void setNullableLong(PreparedStatement ps, int index, Long value) throws SQLException {
|
||||
if (value == null) ps.setNull(index, Types.BIGINT);
|
||||
else ps.setLong(index, value);
|
||||
}
|
||||
|
||||
private static String normalize(String value) {
|
||||
if (value == null) return null;
|
||||
String normalized = value.trim().toLowerCase();
|
||||
return normalized.isEmpty() ? null : normalized;
|
||||
}
|
||||
|
||||
private static String normalizeBlank(String value) {
|
||||
if (value == null) return null;
|
||||
String normalized = value.trim();
|
||||
return normalized.isEmpty() ? null : normalized;
|
||||
}
|
||||
|
||||
private static boolean isBlank(String value) {
|
||||
return value == null || value.isBlank();
|
||||
}
|
||||
|
||||
private record MergeResult(int state, String deliveredLogin, String routesHash) {}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package shine.db.dao;
|
||||
|
||||
import shine.db.DbController;
|
||||
import shine.db.entities.DmSyncOutboxEntry;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Outbox событий DM для единственного второго access-сервера пользователя. */
|
||||
public final class DmSyncOutboxDAO {
|
||||
private static volatile DmSyncOutboxDAO instance;
|
||||
private final DbController db = DbController.getInstance();
|
||||
|
||||
private DmSyncOutboxDAO() {}
|
||||
|
||||
public static DmSyncOutboxDAO getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (DmSyncOutboxDAO.class) {
|
||||
if (instance == null) instance = new DmSyncOutboxDAO();
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void upsert(
|
||||
String ownerLogin,
|
||||
String primaryMessageKey,
|
||||
String eventId,
|
||||
String secondaryMessageKey,
|
||||
boolean synced,
|
||||
long createdAtMs
|
||||
) throws SQLException {
|
||||
long now = System.currentTimeMillis();
|
||||
String sql = """
|
||||
INSERT INTO dm_sync_outbox (
|
||||
owner_login, primary_message_key, event_id, secondary_message_key,
|
||||
synced, created_at_ms, updated_at_ms
|
||||
) VALUES (LOWER(?), ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (owner_login, primary_message_key) DO UPDATE SET
|
||||
event_id = EXCLUDED.event_id,
|
||||
secondary_message_key = EXCLUDED.secondary_message_key,
|
||||
synced = CASE
|
||||
WHEN dm_sync_outbox.event_id = EXCLUDED.event_id
|
||||
THEN dm_sync_outbox.synced OR EXCLUDED.synced
|
||||
ELSE EXCLUDED.synced
|
||||
END,
|
||||
created_at_ms = CASE
|
||||
WHEN dm_sync_outbox.event_id = EXCLUDED.event_id
|
||||
THEN dm_sync_outbox.created_at_ms
|
||||
ELSE EXCLUDED.created_at_ms
|
||||
END,
|
||||
updated_at_ms = EXCLUDED.updated_at_ms
|
||||
""";
|
||||
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, ownerLogin);
|
||||
ps.setString(2, primaryMessageKey);
|
||||
ps.setString(3, eventId);
|
||||
if (secondaryMessageKey == null || secondaryMessageKey.isBlank()) ps.setNull(4, Types.VARCHAR);
|
||||
else ps.setString(4, secondaryMessageKey.trim());
|
||||
ps.setBoolean(5, synced);
|
||||
ps.setLong(6, createdAtMs);
|
||||
ps.setLong(7, now);
|
||||
ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public List<DmSyncOutboxEntry> listUnsynced(String ownerLogin, int limit) throws SQLException {
|
||||
return listUnsynced(ownerLogin, 0L, "", limit);
|
||||
}
|
||||
|
||||
public List<DmSyncOutboxEntry> listUnsynced(
|
||||
String ownerLogin, long afterCreatedAtMs, String afterPrimaryMessageKey, int limit
|
||||
) throws SQLException {
|
||||
String sql = """
|
||||
SELECT owner_login, primary_message_key, event_id, secondary_message_key,
|
||||
synced, created_at_ms, updated_at_ms
|
||||
FROM dm_sync_outbox
|
||||
WHERE LOWER(owner_login) = LOWER(?) AND synced = FALSE
|
||||
AND (created_at_ms > ? OR (created_at_ms = ? AND primary_message_key > ?))
|
||||
ORDER BY created_at_ms ASC, primary_message_key ASC
|
||||
LIMIT ?
|
||||
""";
|
||||
List<DmSyncOutboxEntry> out = new ArrayList<>();
|
||||
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, ownerLogin);
|
||||
ps.setLong(2, Math.max(0L, afterCreatedAtMs));
|
||||
ps.setLong(3, Math.max(0L, afterCreatedAtMs));
|
||||
ps.setString(4, afterPrimaryMessageKey == null ? "" : afterPrimaryMessageKey);
|
||||
ps.setInt(5, Math.max(1, limit));
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
while (rs.next()) out.add(mapRow(rs));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public boolean hasUnsyncedForServer(String serverLogin) throws SQLException {
|
||||
String sql = """
|
||||
SELECT 1
|
||||
FROM dm_sync_outbox o
|
||||
JOIN user_access_servers_current r
|
||||
ON LOWER(r.user_login) = LOWER(o.owner_login)
|
||||
WHERE o.synced = FALSE AND LOWER(r.server_login) = LOWER(?)
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM user_access_servers_current peer
|
||||
WHERE LOWER(peer.user_login) = LOWER(o.owner_login)
|
||||
AND LOWER(peer.server_login) <> LOWER(?)
|
||||
)
|
||||
LIMIT 1
|
||||
""";
|
||||
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, serverLogin);
|
||||
ps.setString(2, serverLogin);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
return rs.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> listUnsyncedOwnersForServer(String serverLogin) throws SQLException {
|
||||
String sql = """
|
||||
SELECT DISTINCT o.owner_login
|
||||
FROM dm_sync_outbox o
|
||||
JOIN user_access_servers_current r
|
||||
ON LOWER(r.user_login) = LOWER(o.owner_login)
|
||||
WHERE o.synced = FALSE AND LOWER(r.server_login) = LOWER(?)
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM user_access_servers_current peer
|
||||
WHERE LOWER(peer.user_login) = LOWER(o.owner_login)
|
||||
AND LOWER(peer.server_login) <> LOWER(?)
|
||||
)
|
||||
ORDER BY o.owner_login
|
||||
""";
|
||||
List<String> result = new ArrayList<>();
|
||||
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setString(1, serverLogin);
|
||||
ps.setString(2, serverLogin);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
while (rs.next()) result.add(rs.getString("owner_login"));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public int markSynced(String ownerLogin, String eventId) throws SQLException {
|
||||
String sql = """
|
||||
UPDATE dm_sync_outbox
|
||||
SET synced = TRUE, updated_at_ms = ?
|
||||
WHERE LOWER(owner_login) = LOWER(?) AND event_id = ?
|
||||
""";
|
||||
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setLong(1, System.currentTimeMillis());
|
||||
ps.setString(2, ownerLogin);
|
||||
ps.setString(3, eventId);
|
||||
return ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public int markSynced(String ownerLogin, List<String> eventIds) throws SQLException {
|
||||
if (eventIds == null || eventIds.isEmpty()) return 0;
|
||||
int total = 0;
|
||||
try (Connection c = db.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement("""
|
||||
UPDATE dm_sync_outbox
|
||||
SET synced = TRUE, updated_at_ms = ?
|
||||
WHERE LOWER(owner_login) = LOWER(?) AND event_id = ?
|
||||
""")) {
|
||||
long now = System.currentTimeMillis();
|
||||
for (String eventId : eventIds) {
|
||||
if (eventId == null || eventId.isBlank()) continue;
|
||||
ps.setLong(1, now);
|
||||
ps.setString(2, ownerLogin);
|
||||
ps.setString(3, eventId.trim());
|
||||
ps.addBatch();
|
||||
}
|
||||
for (int changed : ps.executeBatch()) if (changed > 0) total += changed;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
public int markAllUnsynced(String ownerLogin) throws SQLException {
|
||||
String sql = """
|
||||
UPDATE dm_sync_outbox
|
||||
SET synced = FALSE, updated_at_ms = ?
|
||||
WHERE LOWER(owner_login) = LOWER(?)
|
||||
""";
|
||||
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
ps.setLong(1, System.currentTimeMillis());
|
||||
ps.setString(2, ownerLogin);
|
||||
return ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public int markAllUnsynced() throws SQLException {
|
||||
try (Connection c = db.getConnection();
|
||||
PreparedStatement ps = c.prepareStatement("UPDATE dm_sync_outbox SET synced = FALSE, updated_at_ms = ?")) {
|
||||
ps.setLong(1, System.currentTimeMillis());
|
||||
return ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public int removeMissingMessages() throws SQLException {
|
||||
String sql = """
|
||||
DELETE FROM dm_sync_outbox o
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM signed_messages m WHERE m.message_key = o.primary_message_key
|
||||
)
|
||||
""";
|
||||
try (Connection c = db.getConnection(); PreparedStatement ps = c.prepareStatement(sql)) {
|
||||
return ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private DmSyncOutboxEntry mapRow(ResultSet rs) throws SQLException {
|
||||
DmSyncOutboxEntry e = new DmSyncOutboxEntry();
|
||||
e.setOwnerLogin(rs.getString("owner_login"));
|
||||
e.setPrimaryMessageKey(rs.getString("primary_message_key"));
|
||||
e.setEventId(rs.getString("event_id"));
|
||||
e.setSecondaryMessageKey(rs.getString("secondary_message_key"));
|
||||
e.setSynced(rs.getBoolean("synced"));
|
||||
e.setCreatedAtMs(rs.getLong("created_at_ms"));
|
||||
e.setUpdatedAtMs(rs.getLong("updated_at_ms"));
|
||||
return e;
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package shine.db.entities;
|
||||
|
||||
public class DmDeliveryStateEntry {
|
||||
public static final int ACCEPTED = 0;
|
||||
public static final int DELIVERED = 1;
|
||||
public static final int FAILED = 3;
|
||||
|
||||
// Совместимость со строками БД, созданными ранней экспериментальной миграцией.
|
||||
public static final int PENDING_NONE = ACCEPTED;
|
||||
public static final int DELIVERED_ONE = DELIVERED;
|
||||
public static final int DELIVERED_ALL = 2;
|
||||
public static final int FAILED_FINAL = FAILED;
|
||||
|
||||
private String outgoingMessageKey;
|
||||
private String eventId;
|
||||
private String baseKey;
|
||||
private String fromLogin;
|
||||
private String toLogin;
|
||||
private String incomingMessageKey;
|
||||
private long createdAtMs;
|
||||
private long deliveryExpiresAtMs;
|
||||
private int deliveryState;
|
||||
private String deliveredServerLogin;
|
||||
private String recipientRoutesHash;
|
||||
private int attemptIndex;
|
||||
private Long nextAttemptAtMs;
|
||||
private Long lastAttemptAtMs;
|
||||
private String lastError;
|
||||
private long updatedAtMs;
|
||||
|
||||
public String getOutgoingMessageKey() { return outgoingMessageKey; }
|
||||
public void setOutgoingMessageKey(String value) { this.outgoingMessageKey = value; }
|
||||
public String getEventId() { return eventId; }
|
||||
public void setEventId(String value) { this.eventId = value; }
|
||||
public String getBaseKey() { return baseKey; }
|
||||
public void setBaseKey(String value) { this.baseKey = value; }
|
||||
public String getFromLogin() { return fromLogin; }
|
||||
public void setFromLogin(String value) { this.fromLogin = value; }
|
||||
public String getToLogin() { return toLogin; }
|
||||
public void setToLogin(String value) { this.toLogin = value; }
|
||||
public String getIncomingMessageKey() { return incomingMessageKey; }
|
||||
public void setIncomingMessageKey(String value) { this.incomingMessageKey = value; }
|
||||
public long getCreatedAtMs() { return createdAtMs; }
|
||||
public void setCreatedAtMs(long value) { this.createdAtMs = value; }
|
||||
public long getDeliveryExpiresAtMs() { return deliveryExpiresAtMs; }
|
||||
public void setDeliveryExpiresAtMs(long value) { this.deliveryExpiresAtMs = value; }
|
||||
public int getDeliveryState() { return deliveryState; }
|
||||
public void setDeliveryState(int value) { this.deliveryState = value; }
|
||||
public String getDeliveredServerLogin() { return deliveredServerLogin; }
|
||||
public void setDeliveredServerLogin(String value) { this.deliveredServerLogin = value; }
|
||||
public String getRecipientRoutesHash() { return recipientRoutesHash; }
|
||||
public void setRecipientRoutesHash(String value) { this.recipientRoutesHash = value; }
|
||||
public int getAttemptIndex() { return attemptIndex; }
|
||||
public void setAttemptIndex(int value) { this.attemptIndex = value; }
|
||||
public Long getNextAttemptAtMs() { return nextAttemptAtMs; }
|
||||
public void setNextAttemptAtMs(Long value) { this.nextAttemptAtMs = value; }
|
||||
public Long getLastAttemptAtMs() { return lastAttemptAtMs; }
|
||||
public void setLastAttemptAtMs(Long value) { this.lastAttemptAtMs = value; }
|
||||
public String getLastError() { return lastError; }
|
||||
public void setLastError(String value) { this.lastError = value; }
|
||||
public long getUpdatedAtMs() { return updatedAtMs; }
|
||||
public void setUpdatedAtMs(long value) { this.updatedAtMs = value; }
|
||||
|
||||
public String deliveryStateCode() {
|
||||
return switch (deliveryState) {
|
||||
case DELIVERED_ONE, DELIVERED_ALL -> "delivered";
|
||||
case FAILED_FINAL -> "failed";
|
||||
default -> "accepted";
|
||||
};
|
||||
}
|
||||
|
||||
public static int parseStateCode(String value) {
|
||||
if (value == null) return ACCEPTED;
|
||||
return switch (value.trim().toLowerCase()) {
|
||||
case "delivered", "delivered_one", "delivered_all" -> DELIVERED;
|
||||
case "failed", "failed_final" -> FAILED;
|
||||
default -> ACCEPTED;
|
||||
};
|
||||
}
|
||||
|
||||
public boolean isDelivered() {
|
||||
return deliveryState == DELIVERED_ONE || deliveryState == DELIVERED_ALL;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package shine.db.entities;
|
||||
|
||||
public class DmSyncOutboxEntry {
|
||||
private String ownerLogin;
|
||||
private String primaryMessageKey;
|
||||
private String eventId;
|
||||
private String secondaryMessageKey;
|
||||
private boolean synced;
|
||||
private long createdAtMs;
|
||||
private long updatedAtMs;
|
||||
|
||||
public String getOwnerLogin() { return ownerLogin; }
|
||||
public void setOwnerLogin(String value) { this.ownerLogin = value; }
|
||||
public String getPrimaryMessageKey() { return primaryMessageKey; }
|
||||
public void setPrimaryMessageKey(String value) { this.primaryMessageKey = value; }
|
||||
public String getEventId() { return eventId; }
|
||||
public void setEventId(String value) { this.eventId = value; }
|
||||
public String getSecondaryMessageKey() { return secondaryMessageKey; }
|
||||
public void setSecondaryMessageKey(String value) { this.secondaryMessageKey = value; }
|
||||
public boolean isSynced() { return synced; }
|
||||
public void setSynced(boolean value) { this.synced = value; }
|
||||
public long getCreatedAtMs() { return createdAtMs; }
|
||||
public void setCreatedAtMs(long value) { this.createdAtMs = value; }
|
||||
public long getUpdatedAtMs() { return updatedAtMs; }
|
||||
public void setUpdatedAtMs(long value) { this.updatedAtMs = value; }
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dm_delivery_state (
|
||||
outgoing_message_key TEXT PRIMARY KEY,
|
||||
event_id TEXT NOT NULL UNIQUE,
|
||||
base_key TEXT NOT NULL,
|
||||
from_login TEXT NOT NULL,
|
||||
to_login TEXT NOT NULL,
|
||||
incoming_message_key TEXT NOT NULL,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
delivery_expires_at_ms BIGINT NOT NULL,
|
||||
delivery_state INTEGER NOT NULL DEFAULT 0 CHECK (delivery_state IN (0, 1, 2, 3)),
|
||||
delivered_server_login TEXT,
|
||||
recipient_routes_hash TEXT,
|
||||
attempt_index INTEGER NOT NULL DEFAULT 0,
|
||||
next_attempt_at_ms BIGINT,
|
||||
last_attempt_at_ms BIGINT,
|
||||
last_error TEXT,
|
||||
updated_at_ms BIGINT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_dm_delivery_state_due
|
||||
ON dm_delivery_state(next_attempt_at_ms, delivery_state)
|
||||
WHERE delivery_state = 0 AND next_attempt_at_ms IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_dm_delivery_state_base
|
||||
ON dm_delivery_state(base_key, from_login);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dm_sync_outbox (
|
||||
owner_login TEXT NOT NULL,
|
||||
primary_message_key TEXT NOT NULL,
|
||||
event_id TEXT NOT NULL,
|
||||
secondary_message_key TEXT,
|
||||
synced BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (owner_login, primary_message_key)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_dm_sync_outbox_unsynced
|
||||
ON dm_sync_outbox(owner_login, created_at_ms, primary_message_key)
|
||||
WHERE synced = FALSE;
|
||||
|
||||
-- До v12 факт межсерверной доставки не сохранялся. Старые исходящие пары
|
||||
-- считаем уже доставленными, чтобы обновление не вызвало повторную рассылку.
|
||||
INSERT INTO dm_delivery_state (
|
||||
outgoing_message_key, event_id, base_key, from_login, to_login,
|
||||
incoming_message_key, created_at_ms, delivery_expires_at_ms,
|
||||
delivery_state, delivered_server_login, recipient_routes_hash,
|
||||
attempt_index, next_attempt_at_ms, last_attempt_at_ms, last_error, updated_at_ms
|
||||
)
|
||||
SELECT
|
||||
outgoing.message_key,
|
||||
outgoing.message_key || ':' || outgoing.revision_time_ms || ':' || outgoing.reencrypted_at_ms,
|
||||
outgoing.base_key,
|
||||
outgoing.from_login,
|
||||
outgoing.to_login,
|
||||
incoming.message_key,
|
||||
outgoing.created_at_ms,
|
||||
outgoing.created_at_ms + 3600000,
|
||||
2,
|
||||
NULL,
|
||||
NULL,
|
||||
9,
|
||||
NULL,
|
||||
NULL,
|
||||
'MIGRATED_AS_DELIVERED',
|
||||
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||
FROM signed_messages outgoing
|
||||
JOIN signed_messages incoming
|
||||
ON incoming.base_key = outgoing.base_key
|
||||
AND incoming.message_type = outgoing.message_type - 1
|
||||
WHERE outgoing.message_type IN (2, 4)
|
||||
ON CONFLICT (outgoing_message_key) DO NOTHING;
|
||||
|
||||
-- Историю до v12 считаем подтверждённой, иначе само обновление вызовет
|
||||
-- массовую повторную передачу. При замене сервера общий reset вернёт FALSE.
|
||||
INSERT INTO dm_sync_outbox (
|
||||
owner_login, primary_message_key, event_id, secondary_message_key,
|
||||
synced, created_at_ms, updated_at_ms
|
||||
)
|
||||
SELECT
|
||||
LOWER(outgoing.from_login),
|
||||
outgoing.message_key,
|
||||
outgoing.message_key || ':' || outgoing.revision_time_ms || ':' || outgoing.reencrypted_at_ms,
|
||||
incoming.message_key,
|
||||
TRUE,
|
||||
outgoing.created_at_ms,
|
||||
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||
FROM signed_messages outgoing
|
||||
JOIN signed_messages incoming
|
||||
ON incoming.base_key = outgoing.base_key
|
||||
AND incoming.message_type = outgoing.message_type - 1
|
||||
WHERE outgoing.message_type IN (2, 4)
|
||||
ON CONFLICT (owner_login, primary_message_key) DO NOTHING;
|
||||
|
||||
-- Входящая копия синхронизируется отдельно между серверами получателя.
|
||||
INSERT INTO dm_sync_outbox (
|
||||
owner_login, primary_message_key, event_id, secondary_message_key,
|
||||
synced, created_at_ms, updated_at_ms
|
||||
)
|
||||
SELECT
|
||||
LOWER(incoming.to_login),
|
||||
incoming.message_key,
|
||||
incoming.message_key || ':' || incoming.revision_time_ms || ':' || incoming.reencrypted_at_ms,
|
||||
NULL,
|
||||
TRUE,
|
||||
incoming.created_at_ms,
|
||||
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||
FROM signed_messages incoming
|
||||
WHERE incoming.message_type IN (1, 3)
|
||||
ON CONFLICT (owner_login, primary_message_key) DO NOTHING;
|
||||
|
||||
-- Tombstone принадлежит обоим участникам и должен попасть на второй сервер
|
||||
-- каждого из них. Для self-DM конфликт безопасно схлопывается.
|
||||
INSERT INTO dm_sync_outbox (
|
||||
owner_login, primary_message_key, event_id, secondary_message_key,
|
||||
synced, created_at_ms, updated_at_ms
|
||||
)
|
||||
SELECT
|
||||
LOWER(owner_login),
|
||||
tombstone.message_key,
|
||||
tombstone.message_key || ':' || tombstone.revision_time_ms || ':' || tombstone.reencrypted_at_ms,
|
||||
NULL,
|
||||
TRUE,
|
||||
tombstone.created_at_ms,
|
||||
CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||
FROM signed_messages tombstone
|
||||
CROSS JOIN LATERAL (VALUES (tombstone.from_login), (tombstone.to_login)) owners(owner_login)
|
||||
WHERE tombstone.message_type IN (5, 6, 7, 8)
|
||||
ON CONFLICT (owner_login, primary_message_key) DO NOTHING;
|
||||
|
||||
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||
VALUES (1, 12, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
schema_version = EXCLUDED.schema_version,
|
||||
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,46 @@
|
||||
BEGIN;
|
||||
|
||||
-- Нормализация экспериментальной схемы доставки v12:
|
||||
-- 0=accepted, 1=delivered, 3=failed. Старое delivered_all (2)
|
||||
-- объединяется с delivered, поскольку ACK одного сервера теперь достаточен.
|
||||
UPDATE dm_delivery_state
|
||||
SET delivery_state = 1,
|
||||
delivered_server_login = NULL,
|
||||
next_attempt_at_ms = NULL,
|
||||
updated_at_ms = CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||
WHERE delivery_state = 2;
|
||||
|
||||
-- Для незавершённых записей перестраиваем только будущую очередь. Повторная
|
||||
-- передача безопасна благодаря messageKey и идемпотентному приёму.
|
||||
UPDATE dm_delivery_state
|
||||
SET delivery_expires_at_ms = created_at_ms + 3600000,
|
||||
attempt_index = CASE
|
||||
WHEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) >= created_at_ms + 3600000 THEN 4
|
||||
WHEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) >= created_at_ms + 1500000 THEN 3
|
||||
WHEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) >= created_at_ms + 300000 THEN 2
|
||||
ELSE 1
|
||||
END,
|
||||
next_attempt_at_ms = CASE
|
||||
WHEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) >= created_at_ms + 3600000
|
||||
THEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||
WHEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) >= created_at_ms + 1500000
|
||||
THEN created_at_ms + 3600000
|
||||
WHEN CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) >= created_at_ms + 300000
|
||||
THEN created_at_ms + 1500000
|
||||
ELSE created_at_ms + 30000
|
||||
END,
|
||||
updated_at_ms = CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)
|
||||
WHERE delivery_state = 0;
|
||||
|
||||
DROP INDEX IF EXISTS idx_dm_delivery_state_due;
|
||||
CREATE INDEX idx_dm_delivery_state_due
|
||||
ON dm_delivery_state(next_attempt_at_ms, delivery_state)
|
||||
WHERE delivery_state = 0 AND next_attempt_at_ms IS NOT NULL;
|
||||
|
||||
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||
VALUES (1, 13, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
schema_version = EXCLUDED.schema_version,
|
||||
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||
|
||||
COMMIT;
|
||||
@@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS db_schema_version (
|
||||
);
|
||||
|
||||
INSERT INTO db_schema_version (id, schema_version, updated_at_ms)
|
||||
VALUES (1, 11, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||
VALUES (1, 13, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT))
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
schema_version = EXCLUDED.schema_version,
|
||||
updated_at_ms = EXCLUDED.updated_at_ms;
|
||||
@@ -808,6 +808,51 @@ CREATE TABLE IF NOT EXISTS dm_sync_peer_state (
|
||||
CREATE INDEX IF NOT EXISTS idx_dm_sync_peer_state_owner
|
||||
ON dm_sync_peer_state(owner_login);
|
||||
|
||||
-- Изменяемое состояние доставки исходящей пары. Подписанные блоки остаются
|
||||
-- неизменяемыми; эта таблица описывает только сетевую доставку пары 1/2 или 3/4.
|
||||
CREATE TABLE IF NOT EXISTS dm_delivery_state (
|
||||
outgoing_message_key TEXT PRIMARY KEY,
|
||||
event_id TEXT NOT NULL UNIQUE,
|
||||
base_key TEXT NOT NULL,
|
||||
from_login TEXT NOT NULL,
|
||||
to_login TEXT NOT NULL,
|
||||
incoming_message_key TEXT NOT NULL,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
delivery_expires_at_ms BIGINT NOT NULL,
|
||||
delivery_state INTEGER NOT NULL DEFAULT 0 CHECK (delivery_state IN (0, 1, 2, 3)),
|
||||
delivered_server_login TEXT,
|
||||
recipient_routes_hash TEXT,
|
||||
attempt_index INTEGER NOT NULL DEFAULT 0,
|
||||
next_attempt_at_ms BIGINT,
|
||||
last_attempt_at_ms BIGINT,
|
||||
last_error TEXT,
|
||||
updated_at_ms BIGINT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_dm_delivery_state_due
|
||||
ON dm_delivery_state(next_attempt_at_ms, delivery_state)
|
||||
WHERE delivery_state = 0 AND next_attempt_at_ms IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_dm_delivery_state_base
|
||||
ON dm_delivery_state(base_key, from_login);
|
||||
|
||||
-- У одного пользователя теперь максимум один второй access-сервер, поэтому
|
||||
-- достаточно одного флага ACK на событие. Курсор по времени больше не нужен.
|
||||
CREATE TABLE IF NOT EXISTS dm_sync_outbox (
|
||||
owner_login TEXT NOT NULL,
|
||||
primary_message_key TEXT NOT NULL,
|
||||
event_id TEXT NOT NULL,
|
||||
secondary_message_key TEXT,
|
||||
synced BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (owner_login, primary_message_key)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_dm_sync_outbox_unsynced
|
||||
ON dm_sync_outbox(owner_login, created_at_ms, primary_message_key)
|
||||
WHERE synced = FALSE;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS message_views_state (
|
||||
viewer_login TEXT NOT NULL,
|
||||
to_bch_name TEXT NOT NULL,
|
||||
|
||||
Reference in New Issue
Block a user