fix: sonarQube замечания

This commit is contained in:
Максименко Никита Владимирович
2026-09-23 09:55:48 +03:00
parent 1cee7d0f0f
commit f0e9c6c60c
7 changed files with 46 additions and 112 deletions
+8 -93
View File
@@ -1,114 +1,29 @@
# `docker compose up` поднимает кластер целиком: redis, node-a, node-b за # Один узел, состояние маскирования/демаскирования — в памяти самого процесса
# nginx-балансировщиком на 8080, плюс Prometheus на 9090 и Grafana на 3000. # (pdguard.store.backend=memory, дефолт). Redis был нужен только чтобы разделить
# # состояние между несколькими репликами; кластерная схема на 4-vCPU хосте под
# Маскирование детерминировано и работает на любом узле, а вот обратный шаг # нагрузкой давала конкуренцию за CPU (см. историю в git) — один узел проще и
# требует общего состояния — отсюда общий Redis между node-a и node-b. # получает весь хост целиком.
#
# Лимиты CPU/RAM: сервер — 4 vCPU. Без лимитов node-a/node-b/prometheus/grafana
# под нагрузкой отжимали CPU у Redis, тот не укладывался в таймаут команд, узлы
# теряли общее состояние и демаскирование съезжало на резервный путь. Лимиты —
# это потолок (docker compose без swarm не умеет в гарантированные reservations),
# но они не дают соседям выесть Redis подчистую.
services: services:
prometheus: pd-guard:
image: prom/prometheus:v2.54.1
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.retention.time=6h
- --web.enable-lifecycle
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
cpus: 0.5
mem_limit: 512m
grafana:
image: grafana/grafana:11.2.0
ports:
- "3000:3000"
environment:
# Демонстрационный стенд: вход без пароля, чтобы жюри не искало учётные данные.
# Для контура с реальными данными это надо снять.
GF_AUTH_ANONYMOUS_ENABLED: "true"
GF_AUTH_ANONYMOUS_ORG_ROLE: Admin
GF_AUTH_DISABLE_LOGIN_FORM: "true"
GF_USERS_DEFAULT_THEME: light
# Сразу открывать дашборд модуля, а не пустую главную.
GF_DASHBOARDS_DEFAULT_HOME_DASHBOARD_PATH: /var/lib/grafana/dashboards/pd-guard.json
volumes:
- ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro
- ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro
- grafana-data:/var/lib/grafana
cpus: 0.3
mem_limit: 512m
redis:
image: redis:7-alpine
command: ["redis-server", "--save", "", "--appendonly", "no", "--maxmemory", "1gb", "--maxmemory-policy", "allkeys-lru"]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 2s
retries: 5
cpus: 1.0
mem_limit: 1200m
node-a: &node
image: pd-guard-spring:jvm image: pd-guard-spring:jvm
cpus: 1.0 ports:
mem_limit: 2200m - "8080:8080"
environment: environment:
PDGUARD_STORE_BACKEND: redis
SPRING_DATA_REDIS_HOST: redis
PDGUARD_MAX_CONCURRENT: "2000" PDGUARD_MAX_CONCURRENT: "2000"
PDGUARD_WARMUP_ITERATIONS: "2000" PDGUARD_WARMUP_ITERATIONS: "2000"
# Вторая ступень распознавания — две модели под разные задачи (см. # Вторая ступень распознавания — две модели под разные задачи (см.
# NameCascade.java): WikiNEuRal размечает имена, ruBERT — составляющие # NameCascade.java): WikiNEuRal размечает имена, ruBERT — составляющие
# адреса. Модели в репозиторий не входят: ./tools/fetch-ner-model.sh. # адреса. Модели в репозиторий не входят: ./tools/fetch-ner-model.sh.
# Пока движок не задан, соответствующая часть ступени выключена и
# сервис работает на одних правилах.
PDGUARD_NER_NAME_ENGINE: wikineural PDGUARD_NER_NAME_ENGINE: wikineural
PDGUARD_NER_NAME_MODEL: /deployments/models/wikineural-ner PDGUARD_NER_NAME_MODEL: /deployments/models/wikineural-ner
PDGUARD_NER_ADDRESS_ENGINE: rubert PDGUARD_NER_ADDRESS_ENGINE: rubert
PDGUARD_NER_ADDRESS_MODEL: /deployments/models/rubert-ner PDGUARD_NER_ADDRESS_MODEL: /deployments/models/rubert-ner
# ВРЕМЕННО для разового разбора формата тестовых payload'ов — пишет сырые ПД
# в логи узла. LOGGING_LEVEL_* не подходит: relaxed binding из env приводит
# имя логгера к нижнему регистру и не совпадает с ru.pdguard.core.Pipeline
# (заглавная P), поэтому уровень задан через -D, где регистр сохраняется.
# Выключить (убрать переменную) перед официальным нагрузочным прогоном.
JAVA_OPTS: "-Dspring.config.additional-location=optional:file:/deployments/config/ -Dlogging.level.ru.pdguard.core.Pipeline=DEBUG"
volumes: volumes:
- ./config:/deployments/config:ro - ./config:/deployments/config:ro
- ./models:/deployments/models:ro - ./models:/deployments/models:ro
depends_on:
redis:
condition: service_healthy
healthcheck: healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"] test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
interval: 10s interval: 10s
timeout: 2s timeout: 2s
retries: 3 retries: 3
node-b:
<<: *node
lb:
image: nginx:1.27-alpine
cpus: 0.3
mem_limit: 128m
ports:
- "8080:80"
volumes:
- ./nginx/lb.conf:/etc/nginx/nginx.conf:ro
depends_on:
node-a:
condition: service_healthy
node-b:
condition: service_healthy
volumes:
prometheus-data:
grafana-data:
@@ -32,9 +32,6 @@ public class SharedIndex {
private static final Logger LOG = LoggerFactory.getLogger(SharedIndex.class); private static final Logger LOG = LoggerFactory.getLogger(SharedIndex.class);
private static final String KEY_BY_ID = "pdg:id:";
private static final String KEY_BY_MASK = "pdg:mask:";
/** Сколько подряд неудач размыкает предохранитель. */ /** Сколько подряд неудач размыкает предохранитель. */
private static final int FAILURES_TO_OPEN = 3; private static final int FAILURES_TO_OPEN = 3;
@@ -51,6 +51,9 @@ public final class NameDictionary {
private static final Path EXTERNAL_FILE = Path.of("config/well-known.txt"); private static final Path EXTERNAL_FILE = Path.of("config/well-known.txt");
/** Разделитель слов: любая последовательность не-буквенных символов. */
private static final String WORD_SPLIT = "\\P{L}+";
/** Порядковые числительные в имени правителя: «Пётр Первый», «Екатерина Вторая». */ /** Порядковые числительные в имени правителя: «Пётр Первый», «Екатерина Вторая». */
private static final String REGNAL_ORDINALS = private static final String REGNAL_ORDINALS =
"перв|втор|трет|четв[её]рт|пят|шест|седьм|восьм|девят|десят"; "перв|втор|трет|четв[её]рт|пят|шест|седьм|восьм|девят|десят";
@@ -111,7 +114,7 @@ public final class NameDictionary {
* приходится не больше четырёх обращений к хеш-таблице. * приходится не больше четырёх обращений к хеш-таблице.
*/ */
public static boolean containsGivenName(String value) { public static boolean containsGivenName(String value) {
for (String word : value.split("\\P{L}+")) { for (String word : value.split(WORD_SPLIT)) {
String lower = word.toLowerCase(Locale.ROOT); String lower = word.toLowerCase(Locale.ROOT);
// Точное совпадение с основой сильнее всего: «Яков» оканчивается на «ов», // Точное совпадение с основой сильнее всего: «Яков» оканчивается на «ов»,
// но это имя, а не фамилия. // но это имя, а не фамилия.
@@ -138,7 +141,7 @@ public final class NameDictionary {
* не подсказывает, что перед нами имя. * не подсказывает, что перед нами имя.
*/ */
public static boolean containsNamePart(String value) { public static boolean containsNamePart(String value) {
for (String word : value.split("\\P{L}+")) { for (String word : value.split(WORD_SPLIT)) {
String lower = word.toLowerCase(Locale.ROOT); String lower = word.toLowerCase(Locale.ROOT);
if (GIVEN_NAMES.contains(lower)) { if (GIVEN_NAMES.contains(lower)) {
return true; return true;
@@ -155,9 +158,19 @@ public final class NameDictionary {
return lower.matches(".*(?:ович|евич|овна|евна|ична|ичн)$"); return lower.matches(".*(?:ович|евич|овна|евна|ична|ичн)$");
} }
/** Фамилия по словообразованию: Иванов, Петрова, Троицкий, Шевченко. */ /** Окончания, по которым слово похоже на фамилию: Иванов, Петрова, Троицкий, Шевченко. */
private static final Set<String> SURNAME_ENDINGS = Set.of(
"ов", "ев", "ёв", "ин", "ын", "ский", "ская", "ского", "ской", "ском",
"цкий", "цкая", "енко", "ко", "ук", "юк", "ян", "швили", "дзе");
/** Фамилия по словообразованию. Набор окончаний вместо regex: проще и без CANON_EQ. */
private static boolean isSurname(String lower) { private static boolean isSurname(String lower) {
return lower.matches(".*(?:ов|ев|ёв|ин|ын|ск(?:ий|ая|ого|ой|ом)|цк(?:ий|ая)|енко|ко|ук|юк|ян|швили|дзе)$"); for (String ending : SURNAME_ENDINGS) {
if (lower.endsWith(ending)) {
return true;
}
}
return false;
} }
/** /**
@@ -174,7 +187,7 @@ public final class NameDictionary {
return true; return true;
} }
Set<String> stems = currentWellKnownStems(); Set<String> stems = currentWellKnownStems();
for (String word : value.split("\\P{L}+")) { for (String word : value.split(WORD_SPLIT)) {
String lower = word.toLowerCase(Locale.ROOT); String lower = word.toLowerCase(Locale.ROOT);
for (int length = lower.length(); length > 0; length--) { for (int length = lower.length(); length > 0; length--) {
if (stems.contains(lower.substring(0, length))) { if (stems.contains(lower.substring(0, length))) {
@@ -235,7 +248,6 @@ public final class NameDictionary {
} }
} }
return Set.copyOf(merged); return Set.copyOf(merged);
}, });
BUNDLED_WELL_KNOWN_STEMS);
} }
} }
@@ -67,10 +67,9 @@ final class ResourceLoader {
* @param path путь к файлу * @param path путь к файлу
* @param state состояние проверки (время последней проверки и mtime файла) * @param state состояние проверки (время последней проверки и mtime файла)
* @param reader как превратить строки файла в итоговое значение * @param reader как превратить строки файла в итоговое значение
* @param fallback значение, если файл недоступен
*/ */
static <T> T refreshIfChanged(Path path, FileWatchState<T> state, static <T> T refreshIfChanged(Path path, FileWatchState<T> state,
Function<List<String>, T> reader, T fallback) { Function<List<String>, T> reader) {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
if (now - state.lastCheck < state.recheckMillis) { if (now - state.lastCheck < state.recheckMillis) {
return state.current; return state.current;
@@ -33,6 +33,9 @@ public class RuleRegistry {
*/ */
private static final String CAPITALISED = "\\p{Lu}[\\p{Lu}\\p{Ll}]+"; private static final String CAPITALISED = "\\p{Lu}[\\p{Lu}\\p{Ll}]+";
/** Якорное слово-основа: держатель карты, держателем и т.п. */
private static final String HOLDER_STEM = "держател";
/** /**
* Название улицы: от одного до трёх слов с заглавной буквы либо чисел — * Название улицы: от одного до трёх слов с заглавной буквы либо чисел —
* «Тверская», «Малая Никитская», «8 Марта». Ограничение по форме обязательно: * «Тверская», «Малая Никитская», «8 Марта». Ограничение по форме обязательно:
@@ -285,7 +288,7 @@ public class RuleRegistry {
Rule.of(PdTypes.CARDHOLDER, "(?iu:держател\\w*(?:\\s+карты)?|cardholder|на\\s+имя)" Rule.of(PdTypes.CARDHOLDER, "(?iu:держател\\w*(?:\\s+карты)?|cardholder|на\\s+имя)"
+ "\\W{0,10}([A-Z]{2,20}\\s+[A-Z]{2,20})\\b", 86) + "\\W{0,10}([A-Z]{2,20}\\s+[A-Z]{2,20})\\b", 86)
.groups(1) .groups(1)
.anchoredBy("держател", "cardholder", "на имя"), .anchoredBy(HOLDER_STEM, "cardholder", "на имя"),
// --- ФИО --- // --- ФИО ---
@@ -322,7 +325,7 @@ public class RuleRegistry {
.anchoredBy("клиент", "заказчик", "пациент", "сотрудник", "владел", "плательщик", .anchoredBy("клиент", "заказчик", "пациент", "сотрудник", "владел", "плательщик",
"получател", "абонент", "в лице", "представител", "поручител", "заёмщик", "получател", "абонент", "в лице", "представител", "поручител", "заёмщик",
"заемщик", "заявител", "доверител", "вкладчик", "ответственн", "заемщик", "заявител", "доверител", "вкладчик", "ответственн",
"контактное лицо", "исполнител", "держател"), "контактное лицо", "исполнител", HOLDER_STEM),
// «клиент иван иванов», «поручитель петрович» — строчные имена после // «клиент иван иванов», «поручитель петрович» — строчные имена после
// ролевого слова. Регистр снимает требование к заглавной букве, а словарь // ролевого слова. Регистр снимает требование к заглавной букве, а словарь
@@ -339,7 +342,7 @@ public class RuleRegistry {
.anchoredBy("клиент", "заказчик", "пациент", "сотрудник", "владел", "плательщик", .anchoredBy("клиент", "заказчик", "пациент", "сотрудник", "владел", "плательщик",
"получател", "абонент", "в лице", "представител", "поручител", "заёмщик", "получател", "абонент", "в лице", "представител", "поручител", "заёмщик",
"заемщик", "заявител", "доверител", "вкладчик", "ответственн", "заемщик", "заявител", "доверител", "вкладчик", "ответственн",
"контактное лицо", "исполнител", "держател", "отправител", "бенефициар", "контактное лицо", "исполнител", HOLDER_STEM, "отправител", "бенефициар",
"доверенное лицо", "наследник", "созаемщик"), "доверенное лицо", "наследник", "созаемщик"),
// Фамилия рядом с личным именем из словаря: без словаря правило ловило бы // Фамилия рядом с личным именем из словаря: без словаря правило ловило бы
@@ -138,13 +138,16 @@ public final class Validators {
} }
String[] parts = value.split("[.\\-/\\s]+"); String[] parts = value.split("[.\\-/\\s]+");
if (parts.length == 2) { if (parts.length == 2) {
int a = Integer.parseInt(parts[0]); return dayAndMonth(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
int b = Integer.parseInt(parts[1]);
return (a >= 1 && a <= 31 && b >= 1 && b <= 12) || (b >= 1 && b <= 31 && a >= 1 && a <= 12);
} }
if (parts.length != 3) { if (parts.length != 3) {
return false; return false;
} }
return threePartDate(parts);
}
/** {@code 12.05.1985}, {@code 1985-05-12}, {@code 15 03 90} — дата из трёх чисел. */
private static boolean threePartDate(String[] parts) {
int[] n = new int[3]; int[] n = new int[3];
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
if (parts[i].isEmpty() || parts[i].length() > 4) { if (parts[i].isEmpty() || parts[i].length() > 4) {
+6 -1
View File
@@ -37,7 +37,12 @@ pdguard:
systems-file: config/systems.json systems-file: config/systems.json
store: store:
backend: memory backend: memory
max-chars: 134217728 # 128MB (было) держал store.chars упёртым в потолок под держащей нагрузкой —
# FIFO-вытеснение выкидывало свежую запись за миллисекунды, раньше, чем
# приходил её собственный демаскирующий запрос: см. k6 на датасете утечек,
# 6.1% неверных демасков при 2000 VU, при этом 0% на низкой конкурентности.
# На одном узле без соседей по хосту память есть — 512MB даёт запас на порядок.
max-chars: 536870912
ttl-minutes: 30 ttl-minutes: 30
# 32 байта в hex; AES-256 ключ шифрования хранилища # 32 байта в hex; AES-256 ключ шифрования хранилища
encryption-key: "46a38b200c6df557a5fd2c8a57ad3fec6b710b9f3e1fef1451d121a094f63573" encryption-key: "46a38b200c6df557a5fd2c8a57ad3fec6b710b9f3e1fef1451d121a094f63573"