# Исправление утечки соединений базы данных в OJS

## Проблема

На сервере с большим количеством клиентов происходит накопление idle соединений:

- **PostgreSQL**: Запросы становятся `idle` (только 5 из 500 active), в `pgbouncer` `sv_active = 500`, клиенты ждут в очереди
- **MySQL**: Сотни запросов в статусе `SLEEP` в результатах `SHOW FULL PROCESSLIST`

## Причина

OJS не закрывает соединения после обработки каждого запроса:

1. При инициализации приложения (`bootstrap.inc.php`) создаётся `DBConnection`
2. Соединение остаётся открытым после обработки запроса
3. PHP скрипт завершается, но соединение остаётся в базе данных как **idle**
4. При каждом новом запросе создаётся НОВОЕ соединение, старые накапливаются

## Решение

Добавлен механизм автоматического закрытия соединений:

### 1. Добавлен метод `cleanup()` в `DBConnection`
**Файл**: `lib/pkp/classes/db/DBConnection.inc.php`

```php
function cleanup() {
    // Disconnect from database
    if ($this->isConnected()) {
        $this->disconnect();
    }
    // Clear instance from registry
    Registry::set('dbInstance', null, true);
}
```

Этот метод:
- Разрывает соединение с БД
- Удаляет экземпляр из реестра, позволяя создать новое при необходимости

### 2. Добавлена функция `cleanupDatabaseConnection()` в `functions.inc.php`
**Файл**: `lib/pkp/includes/functions.inc.php`

```php
function cleanupDatabaseConnection() {
    // Безопасная проверка доступности классов
    if (!class_exists('DBConnection')) return;
    if (!class_exists('Registry')) return;
    
    $dbConnection = &DBConnection::getInstance();
    if ($dbConnection !== null && is_object($dbConnection)) {
        if (method_exists($dbConnection, 'cleanup')) {
            $dbConnection->cleanup();
        }
    }
}
```

### 3. Зарегистрирована shutdown функция в `bootstrap.inc.php`
**Файл**: `lib/pkp/includes/bootstrap.inc.php`

```php
register_shutdown_function('cleanupDatabaseConnection');
```

Это гарантирует:
- ✅ Функция вызывается при завершении скрипта
- ✅ Вызывается даже если происходит fatal error
- ✅ Вызывается даже при использовании `exit()` или `die()`
- ✅ Безопасна для всех entry points приложения

## Как это работает

```
Запрос начинается
    ↓
bootstrap.inc.php загружается → регистрирует shutdown функцию
    ↓
Application инициализируется → создаётся DBConnection
    ↓
Обрабатывается запрос → выполняются SQL запросы
    ↓
Запрос завершается
    ↓
PHP вызывает registered shutdown функции
    ↓
cleanupDatabaseConnection() вызывается → disconnect() → соединение закрывается ✅
    ↓
PHP скрипт завершается
```

## Дополнительные рекомендации для оптимизации

### 1. PostgreSQL с pgbouncer

#### Конфигурация `pgbouncer.ini`:

```ini
[databases]
rcsi_prod = host=localhost port=5432 dbname=rcsi_prod

[pgbouncer]
pool_mode = transaction      # Или session mode если нужно
max_client_conn = 1000
default_pool_size = 25       # Начальный размер пула
min_pool_size = 10           # Минимальный размер пула
reserve_pool_size = 5        # Резерв для спешных запросов
reserve_pool_timeout = 3     # Таймаут для получения резервного соединения

; Таймауты
server_lifetime = 3600       # Закрывать соединения через час
server_idle_timeout = 600    # Закрывать idle соединения через 10 мин
server_connect_timeout = 15  # Таймаут подключения
query_timeout = 0            # Отключить глобальный таймаут
```

#### Мониторинг pgbouncer:

```bash
# Смотреть статус пула
echo "SHOW POOLS;" | psql -U pgbouncer -d pgbouncer -h localhost

# Смотреть статистику
echo "SHOW STATS;" | psql -U pgbouncer -d pgbouncer -h localhost

# Смотреть клиентов
echo "SHOW CLIENTS;" | psql -U pgbouncer -d pgbouncer -h localhost
```

### 2. MySQL Оптимизация

#### my.cnf конфигурация:

```ini
[mysqld]
# Пулинг соединений (не поддерживается в базовом MySQL)
# Используйте ProxySQL или MaxScale

# Таймауты
wait_timeout = 28800            # 8 часов неактивности
interactive_timeout = 28800     # 8 часов для интерактивных сессий
net_read_timeout = 30           # Таймаут чтения
net_write_timeout = 30          # Таймаут записи

# Лимиты
max_connections = 500           # Максимум соединений
max_user_connections = 100      # На пользователя

# Performance
thread_cache_size = 100         # Кеширование потоков соединений
thread_stack = 262K             # Размер стека потока
```

#### Использование ProxySQL:

```sql
-- Базовая конфигурация пула
INSERT INTO mysql_servers(hostgroup_id, hostname, port, weight) 
VALUES (0, 'localhost', 3306, 1000);

INSERT INTO mysql_query_rules(rule_id, match_digest, destination_hostgroup)
VALUES (1, '.*', 0);

INSERT INTO mysql_users(username, password, active, default_hostgroup)
VALUES ('journals_sandbox', 'password_hash', 1, 0);

-- Применить
LOAD MYSQL SERVERS TO RUNTIME;
LOAD MYSQL USERS TO RUNTIME;
LOAD MYSQL QUERY RULES TO RUNTIME;
```

### 3. Мониторинг OJS

#### Скрипт проверки соединений:

```bash
#!/bin/bash

# PostgreSQL
check_postgres() {
    psql -U journals_sandbox -d rcsi_prod -h localhost -c \
    "SELECT count(*) as total, 
            state, 
            count(*) as count 
     FROM pg_stat_activity 
     GROUP BY state 
     ORDER BY count DESC;"
}

# MySQL
check_mysql() {
    mysql -u journals_sandbox -p -e \
    "SELECT count(*) as total, 
            command, 
            count(*) as count 
     FROM information_schema.processlist 
     GROUP BY command 
     ORDER BY count DESC; 
     SHOW PROCESSLIST LIMIT 20;"
}

# Запустить
check_postgres
echo "---"
check_mysql
```

#### Регулярный мониторинг через cron:

```cron
*/5 * * * * /usr/local/bin/check_db_connections.sh >> /var/log/db_monitor.log 2>&1
```

### 4. Настройка OJS (config.inc.php)

```ini
[database]
; Не использовать постоянные соединения (они вызывают проблемы)
persistent = Off

; Включить логирование запросов для отладки (если нужно)
debug = Off

; Убедитесь, что используется правильный драйвер
driver = postgres9  ; или mysql для MySQL

; Таймауты на уровне приложения
connection_charset = utf8mb4
```

### 5. Отладка проблем

Если проблема всё ещё наблюдается после применения фикса:

#### Проверить, что cleanup вызывается:

Добавить временный лог в `cleanupDatabaseConnection()`:

```php
if (!function_exists('cleanupDatabaseConnection')) {
    function cleanupDatabaseConnection() {
        // Логирование
        error_log('[DB Cleanup] Called at ' . date('Y-m-d H:i:s'));
        
        if (!class_exists('DBConnection')) {
            return;
        }
        if (!class_exists('Registry')) {
            return;
        }
        
        $dbConnection = &DBConnection::getInstance();
        if ($dbConnection !== null && is_object($dbConnection)) {
            if (method_exists($dbConnection, 'cleanup')) {
                error_log('[DB Cleanup] Disconnecting...');
                $dbConnection->cleanup();
                error_log('[DB Cleanup] Done');
            }
        }
    }
}
```

#### Проверить количество соединений в реальном времени:

```bash
# PostgreSQL каждую секунду
watch -n 1 "psql -U journals_sandbox -d rcsi_prod -c \
    \"SELECT count(*) FROM pg_stat_activity;\""

# MySQL каждую секунду
watch -n 1 "mysql -u journals_sandbox -p -e \
    \"SELECT count(*) FROM information_schema.processlist;\""
```

## Проверка успешности фикса

После применения патча вы должны заметить:

✅ **PostgreSQL**:
- Количество `idle` соединений стабилизируется
- `active` соединений всегда низко
- pgbouncer перестанет накапливать соединения

✅ **MySQL**:
- Количество `SLEEP` соединений стабилизируется
- `SHOW PROCESSLIST` покажет намного меньше соединений

✅ **Производительность**:
- Улучшится отклик приложения
- Снизится нагрузка на БД
- Меньше timeout ошибок

## Связанные проблемы

### Если используется connection pooling (pgbouncer/ProxySQL):

Убедитесь, что:
1. ✅ OJS правильно закрывает соединения (этот фикс это делает)
2. ✅ Pooler правильно переиспользует соединения
3. ✅ Таймауты pooler'а выше, чем в БД

### Если проблемы остаются:

1. Проверьте long-running queries:
   ```sql
   -- PostgreSQL
   SELECT pid, usename, application_name, state, query, 
          now() - query_start as duration
   FROM pg_stat_activity
   WHERE state != 'idle'
   ORDER BY query_start;
   ```

2. Проверьте долгие транзакции:
   ```sql
   -- MySQL
   SELECT * FROM information_schema.processlist 
   WHERE command NOT IN ('Sleep', 'Binlog Dump')
   ORDER BY time DESC;
   ```

## Файлы, которые были изменены

1. `/var/www/prod.mia-letum.ru/lib/pkp/classes/db/DBConnection.inc.php`
   - Добавлен метод `cleanup()`

2. `/var/www/prod.mia-letum.ru/lib/pkp/includes/functions.inc.php`
   - Добавлена функция `cleanupDatabaseConnection()`

3. `/var/www/prod.mia-letum.ru/lib/pkp/includes/bootstrap.inc.php`
   - Добавлена регистрация `register_shutdown_function('cleanupDatabaseConnection')`

## Заключение

Эти изменения **критичны** для работы OJS на серверах с высокой нагрузкой. Соединения теперь будут правильно закрываться после каждого запроса, предотвращая накопление idle соединений.

Рекомендуется также применить указанные выше конфигурационные изменения для максимальной оптимизации.
