diff --git a/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java b/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java index 62d50990..85800e61 100644 --- a/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java +++ b/SHiNE-server/shine-server-db/src/main/java/shine/db/DatabaseInitializer.java @@ -18,7 +18,9 @@ public final class DatabaseInitializer { public static final String DB_SCHEMA_VERSION_TABLE = "db_schema_version"; public static final int SCHEMA_VERSION_1 = 1; + public static final int SCHEMA_VERSION_2 = 2; public static final String POSTGRES_SCHEMA_RESOURCE = "postgres/schema_v1.sql"; + public static final String POSTGRES_MIGRATION_V2_RESOURCE = "postgres/migration_v2.sql"; private DatabaseInitializer() {} @@ -73,10 +75,14 @@ public final class DatabaseInitializer { } try (Connection conn = openConnection(jdbcUrl, user, password)) { - if (postgresSchemaVersionTableExists(conn)) { + if (!postgresSchemaVersionTableExists(conn)) { + runSqlScript(conn, POSTGRES_SCHEMA_RESOURCE); return; } - runPostgresSchemaScript(conn); + int currentVersion = readCurrentSchemaVersion(conn); + if (currentVersion < SCHEMA_VERSION_2) { + runSqlScript(conn, POSTGRES_MIGRATION_V2_RESOURCE); + } } } @@ -104,8 +110,23 @@ public final class DatabaseInitializer { } } - private static void runPostgresSchemaScript(Connection conn) throws SQLException { - String sqlScript = loadClasspathResource(POSTGRES_SCHEMA_RESOURCE); + private static int readCurrentSchemaVersion(Connection conn) throws SQLException { + String sql = """ + SELECT schema_version + FROM db_schema_version + WHERE id = 1 + """; + try (Statement st = conn.createStatement(); + ResultSet rs = st.executeQuery(sql)) { + if (!rs.next()) { + return SCHEMA_VERSION_1; + } + return rs.getInt(1); + } + } + + private static void runSqlScript(Connection conn, String resourcePath) throws SQLException { + String sqlScript = loadClasspathResource(resourcePath); List statements = splitSqlStatements(sqlScript); boolean previousAutoCommit = conn.getAutoCommit(); diff --git a/SHiNE-server/shine-server-db/src/main/resources/postgres/migration_v2.sql b/SHiNE-server/shine-server-db/src/main/resources/postgres/migration_v2.sql new file mode 100644 index 00000000..172ea9f3 --- /dev/null +++ b/SHiNE-server/shine-server-db/src/main/resources/postgres/migration_v2.sql @@ -0,0 +1,178 @@ +BEGIN; + +CREATE TABLE IF NOT EXISTS user_access_servers_current ( + user_login TEXT NOT NULL REFERENCES solana_user_pda_current(login) ON DELETE CASCADE, + server_login TEXT NOT NULL REFERENCES solana_user_pda_current(login) ON DELETE CASCADE, + server_url TEXT NOT NULL, + server_client_key TEXT NOT NULL, + user_record_number INTEGER NOT NULL, + user_updated_at_ms BIGINT NOT NULL, + server_record_number INTEGER NOT NULL, + server_updated_at_ms BIGINT NOT NULL, + refreshed_at_ms BIGINT NOT NULL, + PRIMARY KEY (user_login, server_login) +); + +CREATE INDEX IF NOT EXISTS idx_user_access_servers_user + ON user_access_servers_current(user_login); + +CREATE INDEX IF NOT EXISTS idx_user_access_servers_server + ON user_access_servers_current(server_login); + +CREATE OR REPLACE FUNCTION shine_refresh_user_access_servers_for_user(p_user_login TEXT) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +BEGIN + IF p_user_login IS NULL OR btrim(p_user_login) = '' THEN + RETURN; + END IF; + + DELETE FROM user_access_servers_current + WHERE LOWER(user_login) = LOWER(p_user_login); + + INSERT INTO user_access_servers_current ( + user_login, + server_login, + server_url, + server_client_key, + user_record_number, + user_updated_at_ms, + server_record_number, + server_updated_at_ms, + refreshed_at_ms + ) + SELECT + u.login, + s.login, + s.server_address, + s.client_key, + u.record_number, + u.updated_at_ms, + s.record_number, + s.updated_at_ms, + CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) + FROM solana_user_pda_current u + CROSS JOIN LATERAL jsonb_array_elements_text( + CASE + WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb + ELSE u.access_servers_json::jsonb + END + ) AS access_server(login_value) + JOIN solana_user_pda_current s + ON LOWER(s.login) = LOWER(btrim(access_server.login_value)) + AND s.is_server = TRUE + AND btrim(COALESCE(s.server_address, '')) <> '' + WHERE LOWER(u.login) = LOWER(p_user_login) + ON CONFLICT (user_login, server_login) DO UPDATE SET + server_url = EXCLUDED.server_url, + server_client_key = EXCLUDED.server_client_key, + user_record_number = EXCLUDED.user_record_number, + user_updated_at_ms = EXCLUDED.user_updated_at_ms, + server_record_number = EXCLUDED.server_record_number, + server_updated_at_ms = EXCLUDED.server_updated_at_ms, + refreshed_at_ms = EXCLUDED.refreshed_at_ms; +END; +$$; + +CREATE OR REPLACE FUNCTION shine_refresh_user_access_servers_for_server(p_server_login TEXT) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + affected_user RECORD; +BEGIN + IF p_server_login IS NULL OR btrim(p_server_login) = '' THEN + RETURN; + END IF; + + DELETE FROM user_access_servers_current + WHERE LOWER(server_login) = LOWER(p_server_login); + + FOR affected_user IN + SELECT u.login + FROM solana_user_pda_current u + CROSS JOIN LATERAL jsonb_array_elements_text( + CASE + WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb + ELSE u.access_servers_json::jsonb + END + ) AS access_server(login_value) + WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login) + LOOP + PERFORM shine_refresh_user_access_servers_for_user(affected_user.login); + END LOOP; +END; +$$; + +CREATE OR REPLACE FUNCTION shine_refresh_user_access_servers_all() +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + affected_user RECORD; +BEGIN + TRUNCATE TABLE user_access_servers_current; + + FOR affected_user IN + SELECT login + FROM solana_user_pda_current + LOOP + PERFORM shine_refresh_user_access_servers_for_user(affected_user.login); + END LOOP; +END; +$$; + +CREATE OR REPLACE FUNCTION trg_refresh_user_access_servers_from_user_pda_row() +RETURNS TRIGGER +LANGUAGE plpgsql +AS $$ +BEGIN + IF TG_OP = 'DELETE' THEN + PERFORM shine_refresh_user_access_servers_for_user(OLD.login); + PERFORM shine_refresh_user_access_servers_for_server(OLD.login); + RETURN OLD; + END IF; + + IF TG_OP = 'UPDATE' AND LOWER(OLD.login) <> LOWER(NEW.login) THEN + PERFORM shine_refresh_user_access_servers_for_user(OLD.login); + PERFORM shine_refresh_user_access_servers_for_server(OLD.login); + END IF; + + PERFORM shine_refresh_user_access_servers_for_user(NEW.login); + PERFORM shine_refresh_user_access_servers_for_server(NEW.login); + RETURN NEW; +END; +$$; + +CREATE OR REPLACE FUNCTION trg_refresh_user_access_servers_from_user_pda_truncate() +RETURNS TRIGGER +LANGUAGE plpgsql +AS $$ +BEGIN + TRUNCATE TABLE user_access_servers_current; + RETURN NULL; +END; +$$; + +DROP TRIGGER IF EXISTS trg_refresh_user_access_servers_row ON solana_user_pda_current; +CREATE TRIGGER trg_refresh_user_access_servers_row +AFTER INSERT OR UPDATE OR DELETE ON solana_user_pda_current +FOR EACH ROW +EXECUTE FUNCTION trg_refresh_user_access_servers_from_user_pda_row(); + +DROP TRIGGER IF EXISTS trg_refresh_user_access_servers_truncate ON solana_user_pda_current; +CREATE TRIGGER trg_refresh_user_access_servers_truncate +AFTER TRUNCATE ON solana_user_pda_current +FOR EACH STATEMENT +EXECUTE FUNCTION trg_refresh_user_access_servers_from_user_pda_truncate(); + +SELECT shine_refresh_user_access_servers_all(); + +INSERT INTO db_schema_version (id, schema_version, updated_at_ms) +VALUES (1, 2, 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; diff --git a/SHiNE-server/shine-server-db/src/main/resources/postgres/schema_v1.sql b/SHiNE-server/shine-server-db/src/main/resources/postgres/schema_v1.sql index 41d2352a..ff882997 100644 --- a/SHiNE-server/shine-server-db/src/main/resources/postgres/schema_v1.sql +++ b/SHiNE-server/shine-server-db/src/main/resources/postgres/schema_v1.sql @@ -1,5 +1,5 @@ -- SHiNE PostgreSQL runtime schema v1 --- Дата: 2026-07-24 +-- Дата: 2026-07-28 -- -- Назначение: -- - поднять пустую PostgreSQL БД сервера SHiNE с нуля; @@ -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, 1, CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT)) +VALUES (1, 2, 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; @@ -109,6 +109,173 @@ CREATE TABLE IF NOT EXISTS solana_user_pda_current ( CREATE INDEX IF NOT EXISTS idx_user_pda_current_slot ON solana_user_pda_current(slot); +CREATE TABLE IF NOT EXISTS user_access_servers_current ( + user_login TEXT NOT NULL REFERENCES solana_user_pda_current(login) ON DELETE CASCADE, + server_login TEXT NOT NULL REFERENCES solana_user_pda_current(login) ON DELETE CASCADE, + server_url TEXT NOT NULL, + server_client_key TEXT NOT NULL, + user_record_number INTEGER NOT NULL, + user_updated_at_ms BIGINT NOT NULL, + server_record_number INTEGER NOT NULL, + server_updated_at_ms BIGINT NOT NULL, + refreshed_at_ms BIGINT NOT NULL, + PRIMARY KEY (user_login, server_login) +); + +CREATE INDEX IF NOT EXISTS idx_user_access_servers_user + ON user_access_servers_current(user_login); + +CREATE INDEX IF NOT EXISTS idx_user_access_servers_server + ON user_access_servers_current(server_login); + +CREATE OR REPLACE FUNCTION shine_refresh_user_access_servers_for_user(p_user_login TEXT) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +BEGIN + IF p_user_login IS NULL OR btrim(p_user_login) = '' THEN + RETURN; + END IF; + + DELETE FROM user_access_servers_current + WHERE LOWER(user_login) = LOWER(p_user_login); + + INSERT INTO user_access_servers_current ( + user_login, + server_login, + server_url, + server_client_key, + user_record_number, + user_updated_at_ms, + server_record_number, + server_updated_at_ms, + refreshed_at_ms + ) + SELECT + u.login, + s.login, + s.server_address, + s.client_key, + u.record_number, + u.updated_at_ms, + s.record_number, + s.updated_at_ms, + CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) + FROM solana_user_pda_current u + CROSS JOIN LATERAL jsonb_array_elements_text( + CASE + WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb + ELSE u.access_servers_json::jsonb + END + ) AS access_server(login_value) + JOIN solana_user_pda_current s + ON LOWER(s.login) = LOWER(btrim(access_server.login_value)) + AND s.is_server = TRUE + AND btrim(COALESCE(s.server_address, '')) <> '' + WHERE LOWER(u.login) = LOWER(p_user_login) + ON CONFLICT (user_login, server_login) DO UPDATE SET + server_url = EXCLUDED.server_url, + server_client_key = EXCLUDED.server_client_key, + user_record_number = EXCLUDED.user_record_number, + user_updated_at_ms = EXCLUDED.user_updated_at_ms, + server_record_number = EXCLUDED.server_record_number, + server_updated_at_ms = EXCLUDED.server_updated_at_ms, + refreshed_at_ms = EXCLUDED.refreshed_at_ms; +END; +$$; + +CREATE OR REPLACE FUNCTION shine_refresh_user_access_servers_for_server(p_server_login TEXT) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + affected_user RECORD; +BEGIN + IF p_server_login IS NULL OR btrim(p_server_login) = '' THEN + RETURN; + END IF; + + DELETE FROM user_access_servers_current + WHERE LOWER(server_login) = LOWER(p_server_login); + + FOR affected_user IN + SELECT u.login + FROM solana_user_pda_current u + CROSS JOIN LATERAL jsonb_array_elements_text( + CASE + WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb + ELSE u.access_servers_json::jsonb + END + ) AS access_server(login_value) + WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login) + LOOP + PERFORM shine_refresh_user_access_servers_for_user(affected_user.login); + END LOOP; +END; +$$; + +CREATE OR REPLACE FUNCTION shine_refresh_user_access_servers_all() +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + affected_user RECORD; +BEGIN + TRUNCATE TABLE user_access_servers_current; + + FOR affected_user IN + SELECT login + FROM solana_user_pda_current + LOOP + PERFORM shine_refresh_user_access_servers_for_user(affected_user.login); + END LOOP; +END; +$$; + +CREATE OR REPLACE FUNCTION trg_refresh_user_access_servers_from_user_pda_row() +RETURNS TRIGGER +LANGUAGE plpgsql +AS $$ +BEGIN + IF TG_OP = 'DELETE' THEN + PERFORM shine_refresh_user_access_servers_for_user(OLD.login); + PERFORM shine_refresh_user_access_servers_for_server(OLD.login); + RETURN OLD; + END IF; + + IF TG_OP = 'UPDATE' AND LOWER(OLD.login) <> LOWER(NEW.login) THEN + PERFORM shine_refresh_user_access_servers_for_user(OLD.login); + PERFORM shine_refresh_user_access_servers_for_server(OLD.login); + END IF; + + PERFORM shine_refresh_user_access_servers_for_user(NEW.login); + PERFORM shine_refresh_user_access_servers_for_server(NEW.login); + RETURN NEW; +END; +$$; + +CREATE OR REPLACE FUNCTION trg_refresh_user_access_servers_from_user_pda_truncate() +RETURNS TRIGGER +LANGUAGE plpgsql +AS $$ +BEGIN + TRUNCATE TABLE user_access_servers_current; + RETURN NULL; +END; +$$; + +DROP TRIGGER IF EXISTS trg_refresh_user_access_servers_row ON solana_user_pda_current; +CREATE TRIGGER trg_refresh_user_access_servers_row +AFTER INSERT OR UPDATE OR DELETE ON solana_user_pda_current +FOR EACH ROW +EXECUTE FUNCTION trg_refresh_user_access_servers_from_user_pda_row(); + +DROP TRIGGER IF EXISTS trg_refresh_user_access_servers_truncate ON solana_user_pda_current; +CREATE TRIGGER trg_refresh_user_access_servers_truncate +AFTER TRUNCATE ON solana_user_pda_current +FOR EACH STATEMENT +EXECUTE FUNCTION trg_refresh_user_access_servers_from_user_pda_truncate(); + CREATE TABLE IF NOT EXISTS solana_user_pda_history ( id BIGSERIAL PRIMARY KEY, tx_signature TEXT NOT NULL, diff --git a/SHiNE-server/shine-server-solana-users-sync/src/main/java/sync/storage/postgres/PostgresStorageRepository.java b/SHiNE-server/shine-server-solana-users-sync/src/main/java/sync/storage/postgres/PostgresStorageRepository.java index 52a3e43b..cdbe7d27 100644 --- a/SHiNE-server/shine-server-solana-users-sync/src/main/java/sync/storage/postgres/PostgresStorageRepository.java +++ b/SHiNE-server/shine-server-solana-users-sync/src/main/java/sync/storage/postgres/PostgresStorageRepository.java @@ -996,6 +996,208 @@ public final class PostgresStorageRepository "CREATE INDEX IF NOT EXISTS idx_user_pda_history_slot " + "ON solana_user_pda_history(slot)" ); + + statement.executeUpdate( + "CREATE TABLE IF NOT EXISTS user_access_servers_current (" + + "user_login TEXT NOT NULL REFERENCES solana_user_pda_current(login) ON DELETE CASCADE, " + + "server_login TEXT NOT NULL REFERENCES solana_user_pda_current(login) ON DELETE CASCADE, " + + "server_url TEXT NOT NULL, " + + "server_client_key TEXT NOT NULL, " + + "user_record_number INTEGER NOT NULL, " + + "user_updated_at_ms BIGINT NOT NULL, " + + "server_record_number INTEGER NOT NULL, " + + "server_updated_at_ms BIGINT NOT NULL, " + + "refreshed_at_ms BIGINT NOT NULL, " + + "PRIMARY KEY (user_login, server_login)" + + ")" + ); + + statement.executeUpdate( + "CREATE INDEX IF NOT EXISTS idx_user_access_servers_user " + + "ON user_access_servers_current(user_login)" + ); + statement.executeUpdate( + "CREATE INDEX IF NOT EXISTS idx_user_access_servers_server " + + "ON user_access_servers_current(server_login)" + ); + + statement.execute( + """ + CREATE OR REPLACE FUNCTION shine_refresh_user_access_servers_for_user(p_user_login TEXT) + RETURNS VOID + LANGUAGE plpgsql + AS $$ + BEGIN + IF p_user_login IS NULL OR btrim(p_user_login) = '' THEN + RETURN; + END IF; + + DELETE FROM user_access_servers_current + WHERE LOWER(user_login) = LOWER(p_user_login); + + INSERT INTO user_access_servers_current ( + user_login, + server_login, + server_url, + server_client_key, + user_record_number, + user_updated_at_ms, + server_record_number, + server_updated_at_ms, + refreshed_at_ms + ) + SELECT + u.login, + s.login, + s.server_address, + s.client_key, + u.record_number, + u.updated_at_ms, + s.record_number, + s.updated_at_ms, + CAST(EXTRACT(EPOCH FROM clock_timestamp()) * 1000 AS BIGINT) + FROM solana_user_pda_current u + CROSS JOIN LATERAL jsonb_array_elements_text( + CASE + WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb + ELSE u.access_servers_json::jsonb + END + ) AS access_server(login_value) + JOIN solana_user_pda_current s + ON LOWER(s.login) = LOWER(btrim(access_server.login_value)) + AND s.is_server = TRUE + AND btrim(COALESCE(s.server_address, '')) <> '' + WHERE LOWER(u.login) = LOWER(p_user_login) + ON CONFLICT (user_login, server_login) DO UPDATE SET + server_url = EXCLUDED.server_url, + server_client_key = EXCLUDED.server_client_key, + user_record_number = EXCLUDED.user_record_number, + user_updated_at_ms = EXCLUDED.user_updated_at_ms, + server_record_number = EXCLUDED.server_record_number, + server_updated_at_ms = EXCLUDED.server_updated_at_ms, + refreshed_at_ms = EXCLUDED.refreshed_at_ms; + END; + $$; + """ + ); + + statement.execute( + """ + CREATE OR REPLACE FUNCTION shine_refresh_user_access_servers_for_server(p_server_login TEXT) + RETURNS VOID + LANGUAGE plpgsql + AS $$ + DECLARE + affected_user RECORD; + BEGIN + IF p_server_login IS NULL OR btrim(p_server_login) = '' THEN + RETURN; + END IF; + + DELETE FROM user_access_servers_current + WHERE LOWER(server_login) = LOWER(p_server_login); + + FOR affected_user IN + SELECT u.login + FROM solana_user_pda_current u + CROSS JOIN LATERAL jsonb_array_elements_text( + CASE + WHEN btrim(COALESCE(u.access_servers_json, '')) = '' THEN '[]'::jsonb + ELSE u.access_servers_json::jsonb + END + ) AS access_server(login_value) + WHERE LOWER(btrim(access_server.login_value)) = LOWER(p_server_login) + LOOP + PERFORM shine_refresh_user_access_servers_for_user(affected_user.login); + END LOOP; + END; + $$; + """ + ); + + statement.execute( + """ + CREATE OR REPLACE FUNCTION shine_refresh_user_access_servers_all() + RETURNS VOID + LANGUAGE plpgsql + AS $$ + DECLARE + affected_user RECORD; + BEGIN + TRUNCATE TABLE user_access_servers_current; + + FOR affected_user IN + SELECT login + FROM solana_user_pda_current + LOOP + PERFORM shine_refresh_user_access_servers_for_user(affected_user.login); + END LOOP; + END; + $$; + """ + ); + + statement.execute( + """ + CREATE OR REPLACE FUNCTION trg_refresh_user_access_servers_from_user_pda_row() + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ + BEGIN + IF TG_OP = 'DELETE' THEN + PERFORM shine_refresh_user_access_servers_for_user(OLD.login); + PERFORM shine_refresh_user_access_servers_for_server(OLD.login); + RETURN OLD; + END IF; + + IF TG_OP = 'UPDATE' AND LOWER(OLD.login) <> LOWER(NEW.login) THEN + PERFORM shine_refresh_user_access_servers_for_user(OLD.login); + PERFORM shine_refresh_user_access_servers_for_server(OLD.login); + END IF; + + PERFORM shine_refresh_user_access_servers_for_user(NEW.login); + PERFORM shine_refresh_user_access_servers_for_server(NEW.login); + RETURN NEW; + END; + $$; + """ + ); + + statement.execute( + """ + CREATE OR REPLACE FUNCTION trg_refresh_user_access_servers_from_user_pda_truncate() + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ + BEGIN + TRUNCATE TABLE user_access_servers_current; + RETURN NULL; + END; + $$; + """ + ); + + statement.executeUpdate( + "DROP TRIGGER IF EXISTS trg_refresh_user_access_servers_row ON solana_user_pda_current" + ); + statement.executeUpdate( + "CREATE TRIGGER trg_refresh_user_access_servers_row " + + "AFTER INSERT OR UPDATE OR DELETE ON solana_user_pda_current " + + "FOR EACH ROW " + + "EXECUTE FUNCTION trg_refresh_user_access_servers_from_user_pda_row()" + ); + + statement.executeUpdate( + "DROP TRIGGER IF EXISTS trg_refresh_user_access_servers_truncate ON solana_user_pda_current" + ); + statement.executeUpdate( + "CREATE TRIGGER trg_refresh_user_access_servers_truncate " + + "AFTER TRUNCATE ON solana_user_pda_current " + + "FOR EACH STATEMENT " + + "EXECUTE FUNCTION trg_refresh_user_access_servers_from_user_pda_truncate()" + ); + + statement.execute("SELECT shine_refresh_user_access_servers_all()"); } } diff --git a/VERSION.properties b/VERSION.properties index 0613b3a3..5fc34f78 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ client.version=1.2.355 -server.version=1.2.338 +server.version=1.2.339 diff --git a/deploy/POSTGRESQL_SERVERS_STANDARD.md b/deploy/POSTGRESQL_SERVERS_STANDARD.md index 7fbfc28b..911abef0 100644 --- a/deploy/POSTGRESQL_SERVERS_STANDARD.md +++ b/deploy/POSTGRESQL_SERVERS_STANDARD.md @@ -2,7 +2,11 @@ Этот документ фиксирует целевой стандарт PostgreSQL, который должен использоваться на всех серверных контурах SHiNE после ухода от SQLite. -Пока это не текущий production-state, а согласованная целевая схема, под которую можно писать миграции, deploy-скрипты и server-конфиги. +Важно: + +- целевой стандарт для SHiNE: `PostgreSQL 18.x`; +- текущий факт на production `shineup.me` и `server2.shineup.me` на дату `2026-07-28`: `PostgreSQL 18.4`; +- текущий факт на test/devnet `t1` / `t2` / `t3` / `t4` на дату `2026-07-28`: `PostgreSQL 18.4`. ## Цель @@ -71,19 +75,19 @@ PostgreSQL не должна быть доступна из интернета. Production: ```text -/home/player/SHiNE/postgres/shine_server_db +/home/player/SHiNE/postgres/data ``` Test/devnet: ```text -/home/player/tX/postgres/shine_server_db +/home/player/tX/postgres/data ``` Если на одном сервере появится несколько баз SHiNE-сервисов, раскладывать их по отдельным каталогам: ```text -/home/player/SHiNE/postgres/shine_server_db +/home/player/SHiNE/postgres/data /home/player/SHiNE/postgres/analytics_db /home/player/SHiNE/postgres/other_service_db ``` @@ -142,11 +146,10 @@ services: POSTGRES_DB: ${POSTGRES_SUPERUSER_DB} POSTGRES_USER: ${POSTGRES_SUPERUSER} POSTGRES_PASSWORD: ${POSTGRES_SUPERUSER_PASSWORD} - PGDATA: /var/lib/postgresql/data/pgdata ports: - "127.0.0.1:5432:5432" volumes: - - ${SHINE_POSTGRES_DATA_DIR}:/var/lib/postgresql/data + - ${SHINE_POSTGRES_DATA_DIR}:/var/lib/postgresql - ./initdb:/docker-entrypoint-initdb.d:ro healthcheck: test: @@ -167,15 +170,21 @@ POSTGRES_SUPERUSER_PASSWORD=change_me SHINE_APP_DB=shine_server_db SHINE_APP_USER=shine_server SHINE_APP_PASSWORD=change_me_too -SHINE_POSTGRES_DATA_DIR=/home/player/SHiNE/postgres/shine_server_db +SHINE_POSTGRES_DATA_DIR=/home/player/SHiNE/postgres/data ``` Для test/devnet путь адаптировать под конкретный контур: ```dotenv -SHINE_POSTGRES_DATA_DIR=/home/player/t2/postgres/shine_server_db +SHINE_POSTGRES_DATA_DIR=/home/player/t2/postgres/data ``` +Важно для `postgres:18+`: + +- bind-mount нужно делать на `/var/lib/postgresql`, а не на `/var/lib/postgresql/data`; +- если смонтировать старый путь `/var/lib/postgresql/data`, контейнер `postgres:18` может уйти в restart-loop ещё до инициализации БД; +- после первого старта PostgreSQL сам создаст внутри host-каталога структуру вида `18/docker`. + ## Подключение приложения SHiNE server должен подключаться именно к: @@ -203,11 +212,12 @@ SHiNE server должен подключаться именно к: ## Для будущей миграции SHiNE -При переводе SHiNE server с SQLite на PostgreSQL считать обязательным: +При переводе нового серверного контура SHiNE с SQLite на PostgreSQL считать обязательным: - сначала поднять PostgreSQL по этому стандарту; - затем добавить серверные конфиги подключения; - затем прогнать миграции схемы; - только потом переключать runtime приложения на PostgreSQL. -До фактического rollout на конкретный сервер эта БД может ещё отсутствовать. Этот документ описывает не текущее наличие БД, а обязательный целевой стандарт для всех серверов SHiNE. +Для новых серверов этот документ использовать как обязательный шаблон. +На `2026-07-28` и production, и test/devnet контуры SHiNE уже работают на `PostgreSQL 18.4`. diff --git a/deploy/PRODUCTION_SERVERS.md b/deploy/PRODUCTION_SERVERS.md index 0d9b82d9..f9152cc9 100644 --- a/deploy/PRODUCTION_SERVERS.md +++ b/deploy/PRODUCTION_SERVERS.md @@ -2,6 +2,13 @@ Production-контуров два. В документах и скриптах использовать домены, а не IP: физический VPS можно заменить без изменения deploy-логики. +Текущее фактическое состояние на `2026-07-28`: + +- `shineup.me` и `server2.shineup.me` оба уже переведены на `PostgreSQL 18.4`; +- оба production используют локальный Docker-контейнер `shine-postgres`; +- оба production работают без `SQLite`; +- оба production используют Helius mainnet RPC/WS для Solana users sync и runtime Solana RPC. + ## Основной production - Домен: `shineup.me` @@ -16,6 +23,17 @@ Production-контуров два. В документах и скриптах - Caddy site: `shineup.me` - WebSocket: `/ws` -> `127.0.0.1:7070` - Solana cluster: `mainnet-beta` +- Solana RPC/WS: Helius mainnet +- Runtime DB: PostgreSQL в Docker +- PostgreSQL container: `shine-postgres` +- PostgreSQL image: `postgres:18` +- PostgreSQL engine version на `2026-07-28`: `18.4` +- PostgreSQL data dir: `/home/player/SHiNE/postgres` +- PostgreSQL app DB: `shine_main_db` +- PostgreSQL app user: `shine_main` +- Web push: отдельный production VAPID key для `shineup.me` +- TURN для звонков: `turn1.shineup.me`, `turn2.shineup.me` +- SQLite runtime больше не используется Deploy: @@ -38,6 +56,17 @@ bash deploy/scripts/production_shineupme_ui.sh - Caddy site: `server2.shineup.me` - WebSocket: `/ws` -> `127.0.0.1:7070` - Solana cluster: `mainnet-beta` +- Solana RPC/WS: Helius mainnet +- Runtime DB: PostgreSQL в Docker +- PostgreSQL container: `shine-postgres` +- PostgreSQL image: `postgres:18` +- PostgreSQL engine version на `2026-07-28`: `18.4` +- PostgreSQL data dir: `/home/player/SHiNE/postgres` +- PostgreSQL app DB: `shine_server2_db` +- PostgreSQL app user: `shine_server2` +- Web push: отдельный production VAPID key для `server2.shineup.me` +- TURN для звонков: `turn1.shineup.me`, `turn3.shineup.me` +- SQLite runtime больше не используется Deploy: diff --git a/deploy/README.md b/deploy/README.md index 78ef3dd7..19bd61ea 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -15,6 +15,14 @@ - Второй production: `server2.shineup.me`. - Test/devnet стенд: `t1.shineup.me`, `t2.shineup.me`, `t3.shineup.me`, `t4.shineup.me`. +Краткое текущее состояние на `2026-07-28`: + +- оба production уже на `PostgreSQL 18.4` в отдельных Docker-контейнерах `shine-postgres`; +- весь test/devnet стенд `t1`-`t4` тоже уже на `PostgreSQL 18.4`; +- тестовые контуры используют общий Docker-контейнер `shine-test-postgres`; +- production использует Helius mainnet, test/devnet использует Helius devnet; +- актуальные подробности по всем шести серверам смотреть в `PRODUCTION_SERVERS.md` и `TEST_SERVERS.md`. + ## Основные документы - `PRODUCTION_SERVERS.md` — production-контуры. diff --git a/deploy/SETUP_SERVER_FROM_ZERO.md b/deploy/SETUP_SERVER_FROM_ZERO.md index 7e20b2f7..22dd7c13 100644 --- a/deploy/SETUP_SERVER_FROM_ZERO.md +++ b/deploy/SETUP_SERVER_FROM_ZERO.md @@ -41,16 +41,19 @@ mkdir -p /home/player/tX/UI Каждый сервер должен иметь локальный внешний конфиг в рабочей директории сервиса. +Актуальные production-значения для `shineup.me` и `server2.shineup.me` смотреть в `deploy/PRODUCTION_SERVERS.md`. +Ниже именно шаблон для нового контура, а не снимок текущего production. + Production пример: ```properties server.port=7070 -server.SHiNE.login=shineupme -db.url=jdbc:postgresql://127.0.0.1:5432/shine_server_db -db.user=shine_server +server.SHiNE.login= +db.url=jdbc:postgresql://127.0.0.1:5432/ +db.user= db.password=CHANGE_ME server.ui.indexPath=/home/player/SHiNE/shine-ui/index.html -server.info.url=https://shineup.me +server.info.url=https:// server.info.origin=production solana.cluster=mainnet-beta ``` @@ -59,17 +62,20 @@ Test/devnet пример: ```properties server.port=7102 -server.SHiNE.login=server_t2 -db.url=jdbc:postgresql://127.0.0.1:5432/shine_t2_db -db.user=shine_t2 +server.SHiNE.login= +db.url=jdbc:postgresql://127.0.0.1:5432/ +db.user= db.password=CHANGE_ME server.ui.indexPath=/home/player/t2/UI/index.html server.info.url=https://t2.shineup.me server.info.origin=devnet solana.cluster=devnet -solana.rpcUrl=https://api.devnet.solana.com +solana.rpcUrl=https://devnet.helius-rpc.com/?api-key=0614c894-52d8-4ddc-bbbc-0947ce5ec3a4 ``` +Рекомендуемый образ PostgreSQL для новых контуров: `postgres:18`. +Для `postgres:18+` bind-mount нужно делать на `/var/lib/postgresql`, а не на старый путь `/var/lib/postgresql/data`. + Если указан `db.url` и в выбранной БД ещё нет таблицы `db_schema_version`, сервер сам создаст runtime-схему PostgreSQL при первом старте. diff --git a/deploy/SOLANA_USERS_SYNC_SERVER_SETUP.md b/deploy/SOLANA_USERS_SYNC_SERVER_SETUP.md index 0735d5cb..b1f121ac 100644 --- a/deploy/SOLANA_USERS_SYNC_SERVER_SETUP.md +++ b/deploy/SOLANA_USERS_SYNC_SERVER_SETUP.md @@ -10,17 +10,23 @@ - `solana_sync_state` - `solana_sync_tx_history` - `solana_user_pda_current` + - `user_access_servers_current` - `solana_user_pda_history` - источник истины по пользовательским PDA: `solana_user_pda_current`. +- `user_access_servers_current` — это вторичная локальная проекция для быстрого роутинга DM по access servers; + она автоматически пересобирается из `solana_user_pda_current`, включая backfill для уже существующих пользователей. ## Что должно быть настроено в `application.properties` +Для текущих production-значений `shineup.me` и `server2.shineup.me` смотреть `deploy/PRODUCTION_SERVERS.md`. +Ниже универсальный пример для нового контура. + ```properties solana.users.sync.enabled=true -solana.users.sync.rpcUrl=https://api.devnet.solana.com -solana.users.sync.wsUrl=wss://api.devnet.solana.com/ -solana.users.sync.databaseUrl=jdbc:postgresql://127.0.0.1:5432/shine_server_db -solana.users.sync.dbUser=shine_server +solana.users.sync.rpcUrl=https://devnet.helius-rpc.com/?api-key=0614c894-52d8-4ddc-bbbc-0947ce5ec3a4 +solana.users.sync.wsUrl=wss://devnet.helius-rpc.com/?api-key=0614c894-52d8-4ddc-bbbc-0947ce5ec3a4 +solana.users.sync.databaseUrl=jdbc:postgresql://127.0.0.1:5432/ +solana.users.sync.dbUser= solana.users.sync.dbPassword=CHANGE_ME solana.users.sync.pollIntervalSeconds=300 ``` @@ -43,16 +49,17 @@ SHiNE-server/shine-server-db/src/main/resources/postgres/schema_v1.sql ```bash psql \ - "postgresql://shine_server:CHANGE_ME@127.0.0.1:5432/shine_server_db" \ + "postgresql://:CHANGE_ME@127.0.0.1:5432/" \ -f SHiNE-server/shine-server-db/src/main/resources/postgres/schema_v1.sql ``` Скрипт: - создаёт таблицу версии схемы `db_schema_version`; -- ставит `schema_version = 1`; +- ставит `schema_version = 2`; - создаёт таблицы sync-модуля Solana users; - создаёт server runtime tables; +- создаёт триггеры и функции автоматической актуализации `user_access_servers_current`; - не создаёт удалённые legacy-таблицы старого runtime для пользователей и DM; - использует `signed_messages` как единственную таблицу серверных DM. @@ -75,6 +82,9 @@ cd /home/player/SHiNE/postgres docker compose up -d ``` +Шаблон `deploy/postgres/docker-compose.yml.example` рассчитан на `postgres:18`. +Для `postgres:18+` он монтирует host-каталог в `/var/lib/postgresql`, это важно для корректного старта контейнера. + После старта контейнера: ```bash diff --git a/deploy/TEST_SERVERS.md b/deploy/TEST_SERVERS.md index 95af47d1..164f737c 100644 --- a/deploy/TEST_SERVERS.md +++ b/deploy/TEST_SERVERS.md @@ -9,21 +9,34 @@ Это не production. Все deploy-цели задаются через домены. +Текущее фактическое состояние на `2026-07-28`: + +- все четыре тестовых контура `t1` / `t2` / `t3` / `t4` уже работают без `SQLite`; +- все четыре контура используют `PostgreSQL 18.4`; +- PostgreSQL на test-хосте общий для всех четырёх контуров и запущен в Docker-контейнере `shine-test-postgres`; +- все четыре контура используют один и тот же Helius devnet RPC/WS endpoint; +- во всех четырёх UI включён web push client-код и встроен один и тот же test `VAPID` public key. + ## Общие параметры - SSH: `player@tX.shineup.me` - Solana cluster: `devnet` -- Solana RPC: `https://api.devnet.solana.com` +- Solana RPC: `https://devnet.helius-rpc.com/?api-key=0614c894-52d8-4ddc-bbbc-0947ce5ec3a4` +- Solana WS: `wss://devnet.helius-rpc.com/?api-key=0614c894-52d8-4ddc-bbbc-0947ce5ec3a4` - Caddy config: `/etc/caddy/Caddyfile` +- Общий PostgreSQL container: `shine-test-postgres` +- PostgreSQL image: `postgres:18` +- PostgreSQL engine version на `2026-07-28`: `18.4` +- Web push в UI: общий test `VAPID` public key `BOdoWZndZRaNe9kyUFsJ5-xEfFABXNKennAKg15Z7ycAwUIQ7yDV_sIWWYJCwJriN4g9oU-CyJPrn1U6lfxuDbI` ## Инстансы -| Домен | Логин сервера | Сервер | UI | Порт | systemd | -|---|---|---|---|---|---| -| `t1.shineup.me` | `server_t1` | `/home/player/t1/server` | `/home/player/t1/UI` | `7101` | `shine-t1.service` | -| `t2.shineup.me` | `server_t2` | `/home/player/t2/server` | `/home/player/t2/UI` | `7102` | `shine-t2.service` | -| `t3.shineup.me` | `server_t3` | `/home/player/t3/server` | `/home/player/t3/UI` | `7103` | `shine-t3.service` | -| `t4.shineup.me` | `server_t4` | `/home/player/t4/server` | `/home/player/t4/UI` | `7104` | `shine-t4.service` | +| Домен | Логин сервера | Сервер | UI | Порт | systemd | PostgreSQL DB | PostgreSQL user | +|---|---|---|---|---|---|---|---| +| `t1.shineup.me` | `server_t1` | `/home/player/t1/server` | `/home/player/t1/UI` | `7101` | `shine-t1.service` | `shine_t1_db` | `shine_t1` | +| `t2.shineup.me` | `server_t2` | `/home/player/t2/server` | `/home/player/t2/UI` | `7102` | `shine-t2.service` | `shine_t2_db` | `shine_t2` | +| `t3.shineup.me` | `server_t3` | `/home/player/t3/server` | `/home/player/t3/UI` | `7103` | `shine-t3.service` | `shine_t3_db` | `shine_t3` | +| `t4.shineup.me` | `server_t4` | `/home/player/t4/server` | `/home/player/t4/UI` | `7104` | `shine-t4.service` | `shine_t4_db` | `shine_t4` | ## Deploy @@ -54,8 +67,15 @@ curl -I https://t4.shineup.me ```bash sudo systemctl --no-pager --full status caddy shine-t1 shine-t2 shine-t3 shine-t4 +sudo docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' ``` ## Operational-нюанс -Официальный `https://api.devnet.solana.com` может отдавать `HTTP 429`, если несколько инстансов одновременно делают bootstrap PDA/sync-серверов. Если после деплоя какой-то инстанс не подтянул `sync_servers`, рестартовать сервисы по одному с паузой. +Общий Helius devnet endpoint тоже может начать ограничивать запросы, если несколько инстансов одновременно делают тяжёлый bootstrap. Если после деплоя какой-то инстанс не подтянул `sync_servers` или завис на startup sync, рестартовать сервисы по одному с паузой. + +Отдельный нюанс по web push: + +- у `t1`, `t2`, `t3`, `t4` в UI уже есть одинаковый встроенный `VAPID` public key; +- то есть `t3` и `t4` используют тот же web push клиентский код, что и `t1` / `t2`; +- если позже понадобится разделить push-ключи по тестовым контурам, это нужно делать отдельным UI deploy с новым `DEPLOY_WEBPUSH_VAPID_PUBLIC`. diff --git a/deploy/postgres/.env.example b/deploy/postgres/.env.example index cb7c44ca..3b7b701c 100644 --- a/deploy/postgres/.env.example +++ b/deploy/postgres/.env.example @@ -4,4 +4,4 @@ POSTGRES_SUPERUSER_PASSWORD=CHANGE_ME_SUPERUSER SHINE_APP_DB=shine_server_db SHINE_APP_USER=shine_server SHINE_APP_PASSWORD=CHANGE_ME_APP -SHINE_POSTGRES_DATA_DIR=/home/player/SHiNE/postgres/shine_server_db +SHINE_POSTGRES_DATA_DIR=/home/player/SHiNE/postgres/data diff --git a/deploy/postgres/docker-compose.yml.example b/deploy/postgres/docker-compose.yml.example index caa60a08..509e5ba1 100644 --- a/deploy/postgres/docker-compose.yml.example +++ b/deploy/postgres/docker-compose.yml.example @@ -9,11 +9,10 @@ services: POSTGRES_DB: ${POSTGRES_SUPERUSER_DB} POSTGRES_USER: ${POSTGRES_SUPERUSER} POSTGRES_PASSWORD: ${POSTGRES_SUPERUSER_PASSWORD} - PGDATA: /var/lib/postgresql/data/pgdata ports: - "127.0.0.1:5432:5432" volumes: - - ${SHINE_POSTGRES_DATA_DIR}:/var/lib/postgresql/data + - ${SHINE_POSTGRES_DATA_DIR}:/var/lib/postgresql healthcheck: test: - CMD-SHELL diff --git a/deploy/scripts/test_t1_ui.sh b/deploy/scripts/test_t1_ui.sh index c097b1fc..1a45b40f 100755 --- a/deploy/scripts/test_t1_ui.sh +++ b/deploy/scripts/test_t1_ui.sh @@ -8,6 +8,6 @@ EXPECTED_CADDY_SITE="t1.shineup.me" \ DEPLOY_SERVER_LOGIN="server_t1" \ DEPLOY_SERVER_ADDRESS="t1.shineup.me" \ DEPLOY_SOLANA_CLUSTER="devnet" \ -DEPLOY_SOLANA_ENDPOINT="https://api.devnet.solana.com" \ +DEPLOY_SOLANA_ENDPOINT="https://devnet.helius-rpc.com/?api-key=0614c894-52d8-4ddc-bbbc-0947ce5ec3a4" \ TARGET_URL="https://t1.shineup.me" \ bash "$(dirname "$0")/deploy_ui.sh" diff --git a/deploy/scripts/test_t2_ui.sh b/deploy/scripts/test_t2_ui.sh index fbeeaf61..345ba0d2 100755 --- a/deploy/scripts/test_t2_ui.sh +++ b/deploy/scripts/test_t2_ui.sh @@ -8,6 +8,6 @@ EXPECTED_CADDY_SITE="t2.shineup.me" \ DEPLOY_SERVER_LOGIN="server_t2" \ DEPLOY_SERVER_ADDRESS="t2.shineup.me" \ DEPLOY_SOLANA_CLUSTER="devnet" \ -DEPLOY_SOLANA_ENDPOINT="https://api.devnet.solana.com" \ +DEPLOY_SOLANA_ENDPOINT="https://devnet.helius-rpc.com/?api-key=0614c894-52d8-4ddc-bbbc-0947ce5ec3a4" \ TARGET_URL="https://t2.shineup.me" \ bash "$(dirname "$0")/deploy_ui.sh" diff --git a/deploy/scripts/test_t3_ui.sh b/deploy/scripts/test_t3_ui.sh index fd76ef8e..108d4f5b 100755 --- a/deploy/scripts/test_t3_ui.sh +++ b/deploy/scripts/test_t3_ui.sh @@ -8,6 +8,6 @@ EXPECTED_CADDY_SITE="t3.shineup.me" \ DEPLOY_SERVER_LOGIN="server_t3" \ DEPLOY_SERVER_ADDRESS="t3.shineup.me" \ DEPLOY_SOLANA_CLUSTER="devnet" \ -DEPLOY_SOLANA_ENDPOINT="https://api.devnet.solana.com" \ +DEPLOY_SOLANA_ENDPOINT="https://devnet.helius-rpc.com/?api-key=0614c894-52d8-4ddc-bbbc-0947ce5ec3a4" \ TARGET_URL="https://t3.shineup.me" \ bash "$(dirname "$0")/deploy_ui.sh" diff --git a/deploy/scripts/test_t4_ui.sh b/deploy/scripts/test_t4_ui.sh index 415f1124..7fb11657 100755 --- a/deploy/scripts/test_t4_ui.sh +++ b/deploy/scripts/test_t4_ui.sh @@ -8,6 +8,6 @@ EXPECTED_CADDY_SITE="t4.shineup.me" \ DEPLOY_SERVER_LOGIN="server_t4" \ DEPLOY_SERVER_ADDRESS="t4.shineup.me" \ DEPLOY_SOLANA_CLUSTER="devnet" \ -DEPLOY_SOLANA_ENDPOINT="https://api.devnet.solana.com" \ +DEPLOY_SOLANA_ENDPOINT="https://devnet.helius-rpc.com/?api-key=0614c894-52d8-4ddc-bbbc-0947ce5ec3a4" \ TARGET_URL="https://t4.shineup.me" \ bash "$(dirname "$0")/deploy_ui.sh" diff --git a/docs/Solana/SOLANA_USERS_SYNC_MODULE_DESIGN.md b/docs/Solana/SOLANA_USERS_SYNC_MODULE_DESIGN.md index 725ede05..132922e6 100644 --- a/docs/Solana/SOLANA_USERS_SYNC_MODULE_DESIGN.md +++ b/docs/Solana/SOLANA_USERS_SYNC_MODULE_DESIGN.md @@ -20,6 +20,8 @@ - сервер запускает sync-модуль до продолжения собственного startup; - server startup ждёт входа модуля в состояние `READY`; - актуальный источник истины по пользователям для нового PostgreSQL runtime-слоя: `solana_user_pda_current`. +- для быстрого локального маршрутизационного lookup серверов доступа поддерживается вторичная проекция + `user_access_servers_current`, которая автоматически пересобирается из `solana_user_pda_current`. --- @@ -193,6 +195,7 @@ WebSocket `programSubscribe` хорош для realtime, но недостато 6. подтянуть актуальное состояние затронутых PDA; 7. обновить: - `solana_user_pda_current` + - `user_access_servers_current` - `solana_user_pda_history` - `solana_sync_state`