PHP: полный мануал с примерами, функциями и ООП
Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Дерево руководства
Глава 89 · После параметров и транзакций CUBRID
CUBRID: ограничить ожидание запроса и понять, какие изменения видит читатель
Два бюджета SELECT, реальная блокировка строки, отказ запроса без частичной операции и видимость чужого COMMIT.
Что ограничивать и какой результат проверять
Когда каталог перестаёт отвечать, полезно сначала разделить причины. Команда может долго считать, ждать занятую строку, иметь слишком большой результат или читать прежнюю версию в своей транзакции. В этой главе короткий список и отчёт получают разные бюджеты; конфликт записи действительно возникает между двумя сессиями; видимость остатка проверяется до и после чужого COMMIT.
Бюджет здесь — допустимое время ожидания операции, а не обещание её скорости. На экране появятся конкретные карточки Кабель / 3 и Клемма / 7, отчёт 2 позиции / 10 единиц, отказ занятого резерва без частичной пометки и новый сохранённый остаток 2. Отдельное сравнение покажет, почему читатель иногда ещё видит 3 после сохранения 7 другим клиентом.
- Connection db → lock timeout: миллисекунды ожидания чужой блокировки в CCI 11.2
- Request screen → query timeout: миллисекунды исполнения выбранной команды
- Transaction reader → isolation: какие версии подтверждённых данных читать
- PHP/HTTP и дочерний процесс → отдельный общий срок; эти четыре API не заменяют его
Представьте очередь к одной ячейке склада. Срок ожидания свободной ячейки похож на lock timeout, общий предел выбранного задания — на query timeout. Но база хранит несколько версий записей и умеет отменять транзакции; обычная очередь этого не объясняет. Поэтому ниже схема всегда привязана к id=1, qty=3 и реальным COMMIT/ROLLBACK.
Сначала нужны подключение и первая таблица, prepare/bind/execute и границы транзакции. Массив параметров — обычный PHP-массив с именованными ключами. Проверка доступного клиента и сервера находится в соседней главе о возможностях соединения.
| Задача | Видимый итог | Проект |
|---|---|---|
| Два типа работы с разным ожиданием | Карточки товаров и итог по категории; бюджеты 250 / 1500 мс. | Экран и отчёт |
| Другой клиент удерживает остаток | Отказ по lock timeout; потом новый резерв 3 → 2. | Занятая строка |
| Ранняя пометка не должна остаться без резерва | Запрос остановлен; после rollback note снова обычный. | Бюджет составной операции |
| Нужно понять видимость чужого COMMIT | READ COMMITTED: 3 → 7; REPEATABLE READ: 3 → 3 → 7. | Два читателя |
Разделить допустимое ожидание экрана и небольшого отчёта
Посетителю нужен короткий список доступных товаров, а администратору — итог по категории. Для этих операций допустимое ожидание различается. Настройка привязана к подготовленному SQL-запросу: 250 мс для списка и 1500 мс для отчёта.
- SELECT title,qty LIMIT 2 → request screen → бюджет 250 мс → Кабель 3, Клемма 7
- GROUP BY category → request report → бюджет 1500 мс → electric: 2 позиции, 10 единиц
- Изменяем screen на 400 мс → report сохраняет 1500 мс
Самый простой вызов и его смысл
$request = cubrid_prepare($db, 'SELECT title,qty FROM products WHERE qty>0 ORDER BY id LIMIT 2');
if ($request === false) { throw new RuntimeException('Не подготовлен список'); }
if (!cubrid_set_query_timeout($request, 250)) {
throw new RuntimeException('Не задан лимит');
}
echo cubrid_get_query_timeout($request); // 250 миллисекунд
// Затем execute, чтение результата и close_request.prepare создаёт request — ресурс конкретной команды. Число 250 передано setter в миллисекундах: 1000 мс = 1 секунда. Getter позволяет проверить, какой лимит записан в этот request. Само чтение getter не исполняет SELECT и не измеряет его длительность.
Конкретный результат самостоятельной программы
| Наблюдение | Результат |
|---|---|
| Лимит экранного SELECT, мс | 250 |
| Лимит отчёта, мс | 1500 |
| Первая карточка | Кабель / остаток 3 |
| Вторая карточка | Клемма / остаток 7 |
| Отчёт по категории | electric / позиций 2 / единиц 10 |
| Новый лимит только экрана, мс | 400 |
| Лимит отчёта остался, мс | 1500 |
| Явные транзакции | да |
Полный проект, файлы и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=c168Config();$db=c168Connect();$runtime=c168Runtime($db);
$name=c168CreateTable($db,$owned,'budgets',[
['id'=>1,'sku'=>'CAB-168','title'=>'Кабель','category'=>'electric','price_cents'=>12900,'qty'=>3],
['id'=>2,'sku'=>'TERM-168','title'=>'Клемма','category'=>'electric','price_cents'=>1500,'qty'=>7]
]);$table=c168Qualified($name);$screen=null;$report=null;
$parameters=cubrid_get_db_parameter($db);
if(!is_array($parameters)){throw new RuntimeException('Не получены параметры соединения.');}
try {
$screen=cubrid_prepare($db,'SELECT title,qty FROM '.$table.' WHERE qty>0 ORDER BY id LIMIT 2');
$report=cubrid_prepare($db,'SELECT category,COUNT(*) AS positions,SUM(qty) AS units FROM '.$table.' GROUP BY category ORDER BY category');
if(!is_resource($screen)||!is_resource($report)){throw new RuntimeException('Не подготовлены два запроса.');}
$defaultScreen=cubrid_get_query_timeout($screen);$defaultReport=cubrid_get_query_timeout($report);
if($defaultScreen===false||$defaultReport===false){throw new RuntimeException('Не прочитан исходный лимит.');}
if(!cubrid_set_query_timeout($screen,250)||!cubrid_set_query_timeout($report,1500)){
throw new RuntimeException('Лимиты запросов не заданы.');
}
$screenMs=cubrid_get_query_timeout($screen);$reportMs=cubrid_get_query_timeout($report);
if(!cubrid_execute($screen)){throw new RuntimeException('Экранный SELECT не завершён.');}
$items=c168Rows($screen);
if(!cubrid_execute($report)){throw new RuntimeException('Отчёт не завершён.');}
$summary=c168Rows($report);
// Changing one request does not silently reconfigure the other request.
if(!cubrid_set_query_timeout($screen,400)){throw new RuntimeException('Лимит экрана не изменён.');}
$afterChange=cubrid_get_query_timeout($screen);$reportStill=cubrid_get_query_timeout($report);
} finally {c168Release($screen);c168Release($report);}
c168Commit($db);
$rows=[['Лимит экранного SELECT, мс',$screenMs],['Лимит отчёта, мс',$reportMs],
['Первая карточка',$items[0]['title'].' / остаток '.$items[0]['qty']],
['Вторая карточка',$items[1]['title'].' / остаток '.$items[1]['qty']],
['Отчёт по категории',$summary[0]['category'].' / позиций '.$summary[0]['positions'].' / единиц '.$summary[0]['units']],
['Новый лимит только экрана, мс',$afterChange],['Лимит отчёта остался, мс',$reportStill],
['Явные транзакции',(int)$parameters['PARAM_AUTO_COMMIT']===0]];
$checks=['screen_budget'=>$screenMs===250,'report_budget'=>$reportMs===1500,
'screen_rows'=>$items===[['title'=>'Кабель','qty'=>'3'],['title'=>'Клемма','qty'=>'7']],
'report_value'=>count($summary)===1&&$summary[0]['category']==='electric'&&(int)$summary[0]['positions']===2&&(int)$summary[0]['units']===10,
'independent_budgets'=>$afterChange===400&&$reportStill===1500,
'explicit_transactions'=>(int)$parameters['PARAM_AUTO_COMMIT']===0,
'requests_closed'=>$screen===null&&$report===null];
$observations=['connection_parameters'=>$parameters,'request_defaults_ms'=>[$defaultScreen,$defaultReport],
'screen'=>$items,'summary'=>$summary,'budgets_ms'=>['screen'=>250,'report'=>1500,'screen_after'=>400]];
} finally {
if (function_exists('c168LockChildStopped') && !c168LockChildStopped()) {
throw new RuntimeException('Дочерний процесс ещё работает; очистка таблицы запрещена до его остановки.');
}
if (is_resource($db)) {try {$removed=c168Cleanup($db,$owned);} finally {c168Close($db);}}
}
c168Report('cub168-request-budgets','Разделить допустимое ожидание экрана и небольшого отчёта',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Лимит экранного SELECT, мс — 250
Лимит отчёта, мс — 1500
Первая карточка — Кабель / остаток 3
Вторая карточка — Клемма / остаток 7
Отчёт по категории — electric / позиций 2 / единиц 10
Новый лимит только экрана, мс — 400
Лимит отчёта остался, мс — 1500
Явные транзакции — да
Распакуйте полный ZIP и запустите example.php по README.txt в отдельной CUBRID-лаборатории. Одна собственная таблица создаётся и удаляется в finally; result.html показывает значения, result.json — наблюдения, metrics.json — проверки. Реквизиты подключения остаются в закрытом окружении.
Открыть результат · Открыть наблюдения JSON.
Границы решения и типичные ошибки
Эти маленькие SELECT завершаются успешно; программа не утверждает, что работала ровно 250 или 1500 мс. Ограничение исполнения SQL не включает рендеринг HTML, остальные команды страницы или весь HTTP-запрос. Более длинный лимит не исправляет плохой план. Фильтрация, нужные колонки и агрегирование остаются в SQL.
Практический вопрос: При отказах EntityManager смешались тайм-аут запроса, сетевого чтения и единицы настройки разных провайдеров. Показываем собственный CUBRID request и отдельный бюджет блокировки; оба в миллисекундах современного CCI, с оговоркой исторических секунд. Java API и рекомендации Oracle не переносим.
Изменить требование
Экрану разрешили ждать 500 мс, отчёту — 2000 мс. Как изменить два лимита и не перепутать единицы?
Разбор решения
Задать 500 ресурсу screen и 2000 ресурсу report, затем отдельно прочитать каждый getter. Секунды вручную не передавать: значение 2 здесь означает 2 мс, а не 2 секунды.
Не держать посетителя в ожидании занятого остатка
Два менеджера пытаются изменить один остаток. Первый ещё не закончил транзакцию. Второй должен получить понятный отказ за ограниченное время, а затем выполнить новую операцию, когда товар освобождён. Здесь действительно существуют два независимых соединения.
- Кабель: 3 → владелец меняет на 99, но не подтверждает
- Вторая сессия: UPDATE того же id → ожидание lock максимум 1 секунда
- Отказ → явный ROLLBACK; владелец тоже отменяет 99 → снова 3
- Новая самостоятельная операция резервирует 1 → подтверждённый остаток 2
Самый простой вызов и его смысл
if (!cubrid_set_db_parameter($db, CUBRID_PARAM_LOCK_TIMEOUT, 1000)) {
throw new RuntimeException('Не задано ожидание блокировки');
}
$params = cubrid_get_db_parameter($db);
echo $params['PARAM_LOCK_TIMEOUT']; // 1000 мс в CCI 11.2 / сервере 11.3
// При отказе UPDATE сначала сохраняют диагностику, затем завершают операцию rollback.LOCK_TIMEOUT относится к соединению. Здесь 1000 мс означает одну секунду ожидания чужой блокировки, а не всего SELECT или HTML-страницы. В CCI до 9.0 единицей была секунда; старая PHP-страница сохранила именно её, поэтому версию и единицы сверяют до настройки. Владелец и ожидающий — разные сессии, каждая управляет собственной транзакцией. UPDATE с условием qty>0 проверяет наличие товара в базе, а не в старом PHP-массиве.
Конкретный результат самостоятельной программы
| Наблюдение | Результат |
|---|---|
| Товар | Кабель |
| Ожидание блокировки, мс | 1000 |
| Лимит команды, мс | 5000 |
| Первая попытка остановлена | да |
| Операция явно отменена | да |
| Остаток после двух откатов | 3 |
| Примечание второй позиции | обычный |
| Владелец блокировки завершён | да |
| Новая операция зарезервировала | 1 |
| Итоговый остаток | 2 |
Полный проект, файлы и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
require __DIR__.'/lock-lab.php';
$job=null;$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=c168Config();$job=c168LockStartEarly();$db=c168Connect();$runtime=c168Runtime($db);
$name=c168CreateTable($db,$owned,'lockwait',[
['id'=>1,'sku'=>'CAB-168','title'=>'Кабель','category'=>'electric','price_cents'=>12900,'qty'=>3],
['id'=>2,'sku'=>'TERM-168','title'=>'Клемма','category'=>'electric','price_cents'=>1500,'qty'=>7,'note'=>'обычный']
]);$table=c168Qualified($name);$request=null;$proof=null;
$initial=cubrid_get_db_parameter($db);
if(!is_array($initial)){throw new RuntimeException('Не прочитаны параметры соединения.');}
$attempt=null;$rollbackDone=false;$pendingChanged=false;
try {
if(!cubrid_set_db_parameter($db,CUBRID_PARAM_LOCK_TIMEOUT,1000)){
throw new RuntimeException('Не задано ожидание блокировки.');
}
$params=cubrid_get_db_parameter($db);$lockMs=$params['PARAM_LOCK_TIMEOUT'];
c168Commit($db);
c168LockBegin($job,$name,$owned); // Returns only after the owner really holds row 1.
// No preceding business write in the short lock-wait scenario.
$request=cubrid_prepare($db,'UPDATE '.$table.' SET qty=qty-1 WHERE id=1 AND qty>0');
if($request===false){throw new RuntimeException('Не подготовлена попытка резервирования.');}
if(!cubrid_set_query_timeout($request,5000)){throw new RuntimeException('Не задан лимит запроса.');}
$queryMs=cubrid_get_query_timeout($request);
$started=microtime(true);
$attempt=c168Capture(static function() use($request){return cubrid_execute($request);});
$elapsedMs=(microtime(true)-$started)*1000;
if($attempt['value']!==false||!$attempt['warning']||!$attempt['message_present']
||$attempt['code']!==-75||$attempt['facility']!==1){
throw new RuntimeException('Ожидался диагностированный отказ при реальной блокировке.');
}
c168Release($request);
// End the whole unsuccessful business operation explicitly, before a new attempt.
c168Rollback($db);$rollbackDone=true;
} finally {
if(is_resource($request)){c168Release($request);}
if(!$rollbackDone){c168Rollback($db);}
if($job!==null&&!c168LockChildStopped()){$proof=c168LockStop($job);}
if(!cubrid_set_db_parameter($db,CUBRID_PARAM_LOCK_TIMEOUT,(int)$initial['PARAM_LOCK_TIMEOUT'])){
throw new RuntimeException('Исходное ожидание блокировки не восстановлено.');
}
}
$afterFailure=c168Read($db,'SELECT id,title,qty,note FROM '.$table.' WHERE id IN (1,2) ORDER BY id');
c168Commit($db);
// This is an explicit second operation after the conflicting owner has finished.
$reserved=c168Command($db,'UPDATE '.$table.' SET qty=qty-1 WHERE id=1 AND qty>0');
if($reserved!==1){c168Rollback($db);throw new RuntimeException('Новая операция не зарезервировала товар.');}
c168Commit($db);
$final=c168Read($db,'SELECT title,qty FROM '.$table.' WHERE id=1');c168Commit($db);
$restored=cubrid_get_db_parameter($db);
$rows=[['Товар','Кабель'],['Ожидание блокировки, мс',$lockMs],['Лимит команды, мс',$queryMs],
['Первая попытка остановлена',($attempt['value']===false)],['Операция явно отменена',$rollbackDone],
['Остаток после двух откатов',(int)$afterFailure[0]['qty']],
['Примечание второй позиции',$afterFailure[1]['note']],
['Владелец блокировки завершён',$proof['stopped']&&$proof['exit_code']===0],
['Новая операция зарезервировала',1],['Итоговый остаток',(int)$final[0]['qty']]];
$checks=['lock_budget'=>$lockMs===1000,'query_budget'=>$queryMs===5000,
'diagnosed_timeout'=>$attempt['value']===false&&$attempt['code']===-75&&$attempt['facility']===1&&$attempt['warning']&&$attempt['message_present'],
'explicit_rollback'=>$rollbackDone,'no_partial_change'=>$afterFailure[0]['qty']==='3'&&$afterFailure[1]['note']==='обычный',
'owner_rollback'=>$proof['completed']['rolled_back']===true&&$proof['completed']['row']['qty']==='3',
'owner_stopped'=>$proof['stopped']&&$proof['exit_code']===0&&!$proof['termination_by_parent'],
'new_operation'=>$reserved===1&&$final===[['title'=>'Кабель','qty'=>'2']],
'setting_restored'=>$restored['PARAM_LOCK_TIMEOUT']===$initial['PARAM_LOCK_TIMEOUT'],
'request_closed'=>$request===null];
$observations=['timeout_kind'=>'lock_timeout','diagnostic'=>$attempt,'elapsed_ms_observed'=>$elapsedMs,
'lock_milliseconds'=>$lockMs,'query_milliseconds'=>$queryMs,'pending_note_changed_before_failure'=>$pendingChanged,
'after_rollback'=>$afterFailure,'final_card'=>$final,
'lock_child'=>array_intersect_key($proof,array_flip(['started','stopped','exit_code','termination_by_parent',
'deadline_seconds','handshake','completed','launch_handshake'])),
'settings_before'=>$initial,'settings_after'=>$restored];
} finally {
if($job!==null&&!c168LockChildStopped()){c168LockStop($job);}
if (function_exists('c168LockChildStopped') && !c168LockChildStopped()) {
throw new RuntimeException('Дочерний процесс ещё работает; очистка таблицы запрещена до его остановки.');
}
if (is_resource($db)) {try {$removed=c168Cleanup($db,$owned);} finally {c168Close($db);}}
}
c168Report('cub168-lock-timeout','Не держать посетителя в ожидании занятого остатка',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Товар — Кабель
Ожидание блокировки, мс — 1000
Лимит команды, мс — 5000
Первая попытка остановлена — да
Операция явно отменена — да
Остаток после двух откатов — 3
Примечание второй позиции — обычный
Владелец блокировки завершён — да
Новая операция зарезервировала — 1
Итоговый остаток — 2
Распакуйте полный ZIP и запустите example.php по README.txt в отдельной CUBRID-лаборатории. Одна собственная таблица создаётся и удаляется в finally; result.html показывает значения, result.json — наблюдения, metrics.json — проверки. Реквизиты подключения остаются в закрытом окружении. Проект запускает один короткий PHP-процесс до первого подключения родителя, синхронизируется сообщениями, затем подтверждает его остановку до удаления таблицы. Это две реальные сессии, а не выдуманный ответ execute.
Открыть результат · Открыть наблюдения JSON.
Границы решения и типичные ошибки
Синхронизация идёт сообщениями locked/released, а не догадкой «секунды уже хватило». Тайм-аут не освобождает блокировку другого клиента: его транзакция должна закончиться. Новый успех выполнен явно после завершения владельца; это не автоматический бесконечный повтор операции. В отчёте сохранена измеренная длительность, но не обещается точность таймера до миллисекунды.
Изменить требование
Нужно совсем не ждать чужую блокировку. Какое значение относится к этой политике и чем оно отличается от -1?
Разбор решения
Для lock timeout ноль означает отказ без ожидания занятого ресурса, а -1 — отсутствие конечного предела ожидания. Это не те же правила, что у request query timeout. Исходную настройку соединения нужно сохранить и восстановить после локального сценария.
Отменить всю операцию, когда истёк бюджет конкретной команды
Операция сначала помечает вторую позицию, затем резервирует первую. Первая занята другим клиентом. Даже если настройка блокировок допускает 5 секунд ожидания, этому экранному request разрешены только 200 мс. После отказа нельзя подтвердить одну предварительную пометку без самого резервирования.
- Позиция 2: note обычный → ожидает резервирования, пока без COMMIT
- Позиция 1 занята: lock budget 5 секунд; request budget 200 мс
- Команда прекращается → сохраняем ошибку → ROLLBACK всей операции
- Проверяем note обычный и остаток 3; отдельное успешное резервирование даёт 2
Самый простой вызов и его смысл
$request = cubrid_prepare($db, 'UPDATE products SET qty=qty-1 WHERE id=? AND qty>0');
if ($request === false) { throw new RuntimeException('Запрос не подготовлен'); }
if (!cubrid_set_query_timeout($request, 200)) {
throw new RuntimeException('Бюджет команды не установлен');
}
// Перед execute привяжите id через cubrid_bind.
// Неудача команды: сохраните её диагностику и явно отмените бизнес-операцию.В проверенном CCI 11.2 оба значения выражены в миллисекундах, но относятся к разным объектам: lock 5000 у соединения и query 200 у request. Историческая PHP-страница описывает lock timeout в секундах; эта единица относится к старым CCI до 9.0. Нельзя переносить её в современный профиль. Откат всей операции — решение приложения, отделённое от false у execute. Программа проверяет обе товарные строки после явного rollback.
Конкретный результат самостоятельной программы
| Наблюдение | Результат |
|---|---|
| Товар | Кабель |
| Ожидание блокировки, мс | 5000 |
| Лимит команды, мс | 200 |
| Первая попытка остановлена | да |
| Операция явно отменена | да |
| Остаток после двух откатов | 3 |
| Примечание второй позиции | обычный |
| Владелец блокировки завершён | да |
| Новая операция зарезервировала | 1 |
| Итоговый остаток | 2 |
Полный проект, файлы и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
require __DIR__.'/lock-lab.php';
$job=null;$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=c168Config();$job=c168LockStartEarly();$db=c168Connect();$runtime=c168Runtime($db);
$name=c168CreateTable($db,$owned,'querywait',[
['id'=>1,'sku'=>'CAB-168','title'=>'Кабель','category'=>'electric','price_cents'=>12900,'qty'=>3],
['id'=>2,'sku'=>'TERM-168','title'=>'Клемма','category'=>'electric','price_cents'=>1500,'qty'=>7,'note'=>'обычный']
]);$table=c168Qualified($name);$request=null;$proof=null;
$initial=cubrid_get_db_parameter($db);
if(!is_array($initial)){throw new RuntimeException('Не прочитаны параметры соединения.');}
$attempt=null;$rollbackDone=false;$pendingChanged=false;
try {
if(!cubrid_set_db_parameter($db,CUBRID_PARAM_LOCK_TIMEOUT,5000)){
throw new RuntimeException('Не задано ожидание блокировки.');
}
$params=cubrid_get_db_parameter($db);$lockMs=$params['PARAM_LOCK_TIMEOUT'];
c168Commit($db);
c168LockBegin($job,$name,$owned); // Returns only after the owner really holds row 1.
$pendingChanged=c168Command($db,'UPDATE '.$table.' SET note=? WHERE id=2',['ожидает резервирования'])===1;
$request=cubrid_prepare($db,'UPDATE '.$table.' SET qty=qty-1 WHERE id=1 AND qty>0');
if($request===false){throw new RuntimeException('Не подготовлена попытка резервирования.');}
if(!cubrid_set_query_timeout($request,200)){throw new RuntimeException('Не задан лимит запроса.');}
$queryMs=cubrid_get_query_timeout($request);
$started=microtime(true);
$attempt=c168Capture(static function() use($request){return cubrid_execute($request);});
$elapsedMs=(microtime(true)-$started)*1000;
if($attempt['value']!==false||!$attempt['warning']||!$attempt['message_present']
||$attempt['code']!==-4||$attempt['facility']!==1){
throw new RuntimeException('Ожидался диагностированный отказ при реальной блокировке.');
}
c168Release($request);
// End the whole unsuccessful business operation explicitly, before a new attempt.
c168Rollback($db);$rollbackDone=true;
} finally {
if(is_resource($request)){c168Release($request);}
if(!$rollbackDone){c168Rollback($db);}
if($job!==null&&!c168LockChildStopped()){$proof=c168LockStop($job);}
if(!cubrid_set_db_parameter($db,CUBRID_PARAM_LOCK_TIMEOUT,(int)$initial['PARAM_LOCK_TIMEOUT'])){
throw new RuntimeException('Исходное ожидание блокировки не восстановлено.');
}
}
$afterFailure=c168Read($db,'SELECT id,title,qty,note FROM '.$table.' WHERE id IN (1,2) ORDER BY id');
c168Commit($db);
// This is an explicit second operation after the conflicting owner has finished.
$reserved=c168Command($db,'UPDATE '.$table.' SET qty=qty-1 WHERE id=1 AND qty>0');
if($reserved!==1){c168Rollback($db);throw new RuntimeException('Новая операция не зарезервировала товар.');}
c168Commit($db);
$final=c168Read($db,'SELECT title,qty FROM '.$table.' WHERE id=1');c168Commit($db);
$restored=cubrid_get_db_parameter($db);
$rows=[['Товар','Кабель'],['Ожидание блокировки, мс',$lockMs],['Лимит команды, мс',$queryMs],
['Первая попытка остановлена',($attempt['value']===false)],['Операция явно отменена',$rollbackDone],
['Остаток после двух откатов',(int)$afterFailure[0]['qty']],
['Примечание второй позиции',$afterFailure[1]['note']],
['Владелец блокировки завершён',$proof['stopped']&&$proof['exit_code']===0],
['Новая операция зарезервировала',1],['Итоговый остаток',(int)$final[0]['qty']]];
$checks=['lock_budget'=>$lockMs===5000,'query_budget'=>$queryMs===200,
'diagnosed_timeout'=>$attempt['value']===false&&$attempt['code']===-4&&$attempt['facility']===1&&$attempt['warning']&&$attempt['message_present'],
'explicit_rollback'=>$rollbackDone,'no_partial_change'=>$afterFailure[0]['qty']==='3'&&$afterFailure[1]['note']==='обычный',
'owner_rollback'=>$proof['completed']['rolled_back']===true&&$proof['completed']['row']['qty']==='3',
'owner_stopped'=>$proof['stopped']&&$proof['exit_code']===0&&!$proof['termination_by_parent'],
'new_operation'=>$reserved===1&&$final===[['title'=>'Кабель','qty'=>'2']],
'setting_restored'=>$restored['PARAM_LOCK_TIMEOUT']===$initial['PARAM_LOCK_TIMEOUT'],
'request_closed'=>$request===null];
$observations=['timeout_kind'=>'query_timeout','diagnostic'=>$attempt,'elapsed_ms_observed'=>$elapsedMs,
'lock_milliseconds'=>$lockMs,'query_milliseconds'=>$queryMs,'pending_note_changed_before_failure'=>$pendingChanged,
'after_rollback'=>$afterFailure,'final_card'=>$final,
'lock_child'=>array_intersect_key($proof,array_flip(['started','stopped','exit_code','termination_by_parent',
'deadline_seconds','handshake','completed','launch_handshake'])),
'settings_before'=>$initial,'settings_after'=>$restored];
} finally {
if($job!==null&&!c168LockChildStopped()){c168LockStop($job);}
if (function_exists('c168LockChildStopped') && !c168LockChildStopped()) {
throw new RuntimeException('Дочерний процесс ещё работает; очистка таблицы запрещена до его остановки.');
}
if (is_resource($db)) {try {$removed=c168Cleanup($db,$owned);} finally {c168Close($db);}}
}
c168Report('cub168-query-timeout','Отменить всю операцию, когда истёк бюджет конкретной команды',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Товар — Кабель
Ожидание блокировки, мс — 5000
Лимит команды, мс — 200
Первая попытка остановлена — да
Операция явно отменена — да
Остаток после двух откатов — 3
Примечание второй позиции — обычный
Владелец блокировки завершён — да
Новая операция зарезервировала — 1
Итоговый остаток — 2
Распакуйте полный ZIP и запустите example.php по README.txt в отдельной CUBRID-лаборатории. Одна собственная таблица создаётся и удаляется в finally; result.html показывает значения, result.json — наблюдения, metrics.json — проверки. Реквизиты подключения остаются в закрытом окружении. Проект запускает один короткий PHP-процесс до первого подключения родителя, синхронизируется сообщениями, затем подтверждает его остановку до удаления таблицы. Это две реальные сессии, а не выдуманный ответ execute.
Открыть результат · Открыть наблюдения JSON.
Границы решения и типичные ошибки
Обрыв соединения, тайм-аут исполнения и тайм-аут ожидания блокировки — разные отказы. Если соединение разорвано, нельзя считать неизвестный результат успешным и вслепую повторять платёж или заказ. Здесь соединение остаётся рабочим в проверенном профиле, владелец управляемый, а повторная операция разрешена только после подтверждённых откатов и чтения данных.
Практический вопрос: Автор хотел понять, почему отказ одной команды не равен отмене всей составной операции. В оригинальном PHP-проекте перед конфликтующим UPDATE есть ещё одна запись; явный rollback и чтение обеих строк показывают именно бизнес-границу. Контракты JPA не объявляются правилами CUBRID.
Изменить требование
Почему недостаточно после false просто выполнить commit, чтобы сохранить уже сделанную пометку?
Разбор решения
Пометка и резервирование относятся к одной полезной операции. Без резерва пометка вводит в заблуждение. Нужно отменить всю операцию, показать пользователю, что резерв не создан, и начинать новое действие с известного состояния.
Сравнить карточку сейчас и стабильное чтение внутри транзакции
На экране остатков нужно видеть свежие подтверждённые данные. При последовательном формировании одного отчёта может требоваться согласованное повторное чтение. Сравним одну карточку Кабель в двух режимах, пока независимый клиент подтверждает изменение 3 → 7.
- Читатель A открывает транзакцию и видит Кабель: 3
- Писатель B меняет остаток на 7 и выполняет свой COMMIT
- A с уровнем 4 читает 7; A с уровнем 5 внутри прежней транзакции читает 3
- A завершает свою транзакцию → следующее чтение видит 7
Самый простой вызов и его смысл
// Завершите прежнюю операцию до выбора режима для следующей.
if (!cubrid_set_db_parameter($db, CUBRID_PARAM_ISOLATION_LEVEL, 5)) {
throw new RuntimeException('Режим не установлен');
}
$parameters = cubrid_get_db_parameter($db);
echo $parameters['PARAM_ISOLATION_LEVEL']; // 5: REPEATABLE READ в этом профиле
// Далее выполняют связанные чтения и явно завершают транзакцию.Изоляция определяет, какие версии строк доступны транзакции. COMMIT писателя сохраняет его изменения, но не завершает транзакцию читателя. В программе две переменные действительно содержат разные подключения: new_link=true задан в helper. Один процесс достаточен, потому что операции последовательно чередуются и ни одна из них не должна ждать незавершённый UPDATE.
Конкретный результат самостоятельной программы
| Наблюдение | Результат |
|---|---|
| Товар | Кабель |
| READ COMMITTED: первое чтение | 3 |
| READ COMMITTED: после чужого COMMIT | 7 |
| REPEATABLE READ: первое чтение | 3 |
| REPEATABLE READ: после чужого COMMIT | 3 |
| REPEATABLE READ: после собственного COMMIT | 7 |
| Исходный режим восстановлен | да |
Полный проект, файлы и архив
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=c168Config();$db=c168Connect();$runtime=c168Runtime($db);
$name=c168CreateTable($db,$owned,'isolation',[
['id'=>1,'sku'=>'CAB-168','title'=>'Кабель','category'=>'electric','price_cents'=>12900,'qty'=>3]
]);$table=c168Qualified($name);$other=null;$runs=[];
$original=cubrid_get_db_parameter($db);
if(!is_array($original)){throw new RuntimeException('Не получена исходная изоляция.');}
try {
$other=c168Connect(); // new_link=true: another real session, not a second variable for the same connection.
foreach([4,5] as $level){
c168Rollback($db);c168Rollback($other);
c168Command($other,'UPDATE '.$table.' SET qty=3 WHERE id=1');c168Commit($other);
if(!cubrid_set_db_parameter($db,CUBRID_PARAM_ISOLATION_LEVEL,$level)){
throw new RuntimeException('Уровень изоляции не принят.');
}
$active=cubrid_get_db_parameter($db);
$first=c168Read($db,'SELECT title,qty FROM '.$table.' WHERE id=1');
c168Command($other,'UPDATE '.$table.' SET qty=7 WHERE id=1');c168Commit($other);
$second=c168Read($db,'SELECT title,qty FROM '.$table.' WHERE id=1');
c168Commit($db);
$next=c168Read($db,'SELECT title,qty FROM '.$table.' WHERE id=1');c168Commit($db);
$runs[]=['level'=>$active['PARAM_ISOLATION_LEVEL'],'before'=>$first[0],
'after_foreign_commit'=>$second[0],'after_own_commit'=>$next[0]];
}
} finally {
c168Rollback($db);
if(is_resource($other)){c168Rollback($other);c168Close($other);}
if(!cubrid_set_db_parameter($db,CUBRID_PARAM_ISOLATION_LEVEL,(int)$original['PARAM_ISOLATION_LEVEL'])){
throw new RuntimeException('Исходная изоляция не восстановлена.');
}
}
$restored=cubrid_get_db_parameter($db);
$rows=[['Товар','Кабель'],['READ COMMITTED: первое чтение',(int)$runs[0]['before']['qty']],
['READ COMMITTED: после чужого COMMIT',(int)$runs[0]['after_foreign_commit']['qty']],
['REPEATABLE READ: первое чтение',(int)$runs[1]['before']['qty']],
['REPEATABLE READ: после чужого COMMIT',(int)$runs[1]['after_foreign_commit']['qty']],
['REPEATABLE READ: после собственного COMMIT',(int)$runs[1]['after_own_commit']['qty']],
['Исходный режим восстановлен',$restored['PARAM_ISOLATION_LEVEL']===$original['PARAM_ISOLATION_LEVEL']]];
$checks=['levels_readback'=>array_column($runs,'level')===[4,5],
'committed_read'=>$runs[0]['before']['qty']==='3'&&$runs[0]['after_foreign_commit']['qty']==='7',
'repeatable_read'=>$runs[1]['before']['qty']==='3'&&$runs[1]['after_foreign_commit']['qty']==='3',
'new_transaction_sees_commit'=>$runs[1]['after_own_commit']['qty']==='7',
'named_cards'=>$runs[0]['before']['title']==='Кабель'&&$runs[1]['after_own_commit']['title']==='Кабель',
'restored_isolation'=>$restored['PARAM_ISOLATION_LEVEL']===$original['PARAM_ISOLATION_LEVEL'],
'other_session_closed'=>$other===null];
$observations=['two_sessions'=>true,'runs'=>$runs,'initial_parameters'=>$original,'restored_parameters'=>$restored];
} finally {
if (function_exists('c168LockChildStopped') && !c168LockChildStopped()) {
throw new RuntimeException('Дочерний процесс ещё работает; очистка таблицы запрещена до его остановки.');
}
if (is_resource($db)) {try {$removed=c168Cleanup($db,$owned);} finally {c168Close($db);}}
}
c168Report('cub168-read-isolation','Сравнить карточку сейчас и стабильное чтение внутри транзакции',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Товар — Кабель
READ COMMITTED: первое чтение — 3
READ COMMITTED: после чужого COMMIT — 7
REPEATABLE READ: первое чтение — 3
REPEATABLE READ: после чужого COMMIT — 3
REPEATABLE READ: после собственного COMMIT — 7
Исходный режим восстановлен — да
Распакуйте полный ZIP и запустите example.php по README.txt в отдельной CUBRID-лаборатории. Одна собственная таблица создаётся и удаляется в finally; result.html показывает значения, result.json — наблюдения, metrics.json — проверки. Реквизиты подключения остаются в закрытом окружении.
Открыть результат · Открыть наблюдения JSON.
Границы решения и типичные ошибки
Так проверяется повторное чтение одной выбранной строки, а не все возможные фантомы или полная сериализуемость. Старые уровни 1–3 из PHP Manual относятся к исторической модели CLASS/INSTANCES; совместимость версии надо проверять отдельно. Уровень выбирают на границе операции, а не посередине готового отчёта. Большая транзакция также удерживает версии и другие ресурсы сервера.
Практический вопрос: Читатель ожидал одинакового поведения READ COMMITTED в разных СУБД и не понимал, что происходит вокруг чужого COMMIT. Два реальных CUBRID-соединения сравнивают 3→7 при уровнях4 и5. Блокирующее поведение SQL Server и его hints не заимствуются.
Изменить требование
Читатель увидел 3 при REPEATABLE READ, хотя другой клиент уже сохранил 7. Это означает, что запись не сохранилась?
Разбор решения
Нет. Читатель ещё находится в своей прежней транзакции. После её завершения новое чтение покажет 7. Показать немедленно свежий остаток и сохранить единый снимок нескольких чтений — разные требования; режим выбирают под требование.
Четыре функции настройки и чтения политики
| Функция | Для чего |
|---|---|
| cubrid_get_db_parameter | Узнать фактический режим соединения: изоляцию, ожидание блокировки, предельную строку и autocommit. |
| cubrid_set_db_parameter | Задать соединению ожидание блокировки либо изоляцию следующей операции. |
| cubrid_get_query_timeout | Проверить установленный бюджет конкретного request в миллисекундах. |
| cubrid_set_query_timeout | Ограничить допустимое исполнение выбранного request, не меняя автоматически соседние запросы. |
Проверять полезный результат и известное завершение операции
Начинайте с полезной операции: какие строки показать или изменить, какой результат подтвердить и что делать при отказе. Выбирайте нужные поля и условия в SQL, задавайте осмысленную границу транзакции, затем ограничивайте ожидание. После неудачи сохраняйте исходную диагностику и доводите операцию до известного состояния; нулевое время, false и пустой результат имеют разные значения.
Для формы данных вернитесь к представлению результата, для команды с пользовательскими значениями — к параметризованному запросу, для сохранения нескольких действий вместе — к транзакции. Тайм-аут полезен как граница ожидания, но план запроса, индексы и идемпотентность требуют собственных решений.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
cubrid_get_db_parameterУзнать фактический режим соединения: изоляцию, ожидание блокировки, предельную строку и autocommit.
PECL CUBRID >= 8.3.0 · cubrid
cubrid_get_db_parameter(resource $conn_identifier): array|falseУзнать фактический режим соединения: изоляцию, ожидание блокировки, предельную строку и autocommit.
cubrid_get_db_parameter(resource $conn_identifier): array|falseЭто запись справочного контракта. Обозначения resource, int, bool и array описывают значения; строку с resource не нужно копировать как собственное объявление PHP-функции. Версия появления: PECL CUBRID >= 8.3.0. Рабочие программы используют отдельно закреплённые PHP 7.4.33 и native-драйвер 11.2.0.0002.
| Параметр | Смысл и границы |
|---|---|
| conn_identifier | Живое соединение, полученное connect/connect_with_url. В закреплённом native-драйвере аргумент обязателен. Это не ресурс отдельного SQL-request. |
Возвращаемое значение и отказ
Ассоциативный массив четырёх параметров либо false при отказе. Значения числовые. Сначала проверяют is_array; затем читают нужный ключ. Не подставляйте «безопасное значение по умолчанию», если сам getter не сработал. Ошибка из cubrid_execute должна быть сохранена до вызова getter: следующая операция может заменить диагностику.
| Ключ массива | Что означает | Кто меняет |
|---|---|---|
| PARAM_ISOLATION_LEVEL | Изоляция соединения: какие версии данных доступны чтениям транзакции. | cubrid_set_db_parameter |
| PARAM_LOCK_TIMEOUT | В закреплённом CCI 11.2 — миллисекунды ожидания блокировки: положительное значение задаёт предел; 0 — не ждать; -1 — бесконечное ожидание. | cubrid_set_db_parameter |
| PARAM_MAX_STRING_LENGTH | Сообщаемый CCI предел строки в байтах из MAX_STRING_LENGTH broker. Это не длина title, не размер каждой VARCHAR-колонки и не запрос на выделение такого объёма памяти. | Через этот setter не изменяется. |
| PARAM_AUTO_COMMIT | Режим автоматического завершения команд: 0 выключен, 1 включён. | cubrid_set_autocommit |
Имена ключей массива начинаются с PARAM_. Константы, которые передают setter, начинаются с CUBRID_PARAM_. Это разные роли: строка ключа нужна для чтения ответа, константа — для выбора изменяемого параметра.
Важная граница версий: PHP Manual на этой странице описывает LOCK_TIMEOUT в секундах. Справочник CCI 11.3 прямо уточняет: секунды применялись до 9.0, современное значение — миллисекунды. Закреплённый PHP-драйвер передаёт число в CCI без пересчёта. Поэтому в наших программах 1000 означает одну секунду, а 1 — одну миллисекунду; само совпадение getter с setter ещё не доказывает правильность единицы.
Применение 1: проверить режим, в котором будет работать экран
$params = cubrid_get_db_parameter($db);
if (!is_array($params)) { throw new RuntimeException('Нет параметров соединения'); }
echo 'Lock, мс (CCI 11.2): ', $params['PARAM_LOCK_TIMEOUT'], "\n";
echo 'Autocommit: ', $params['PARAM_AUTO_COMMIT'], "\n";
// request query timeout этим массивом не возвращается.Экран и отчёт показывают реальные строки Кабель / 3 и Клемма / 7 и одновременно фиксируют autocommit=0. Лаборатория задаёт его явно. Историческая PHP-страница называет выключенный autocommit исходным режимом; современный CCI наследует broker CCI_DEFAULT_AUTOCOMMIT, обычно ON. Поэтому чужую установку проверяют, а не угадывают по старому примеру.
Применение 2: сохранить настройку перед временной политикой
$saved = cubrid_get_db_parameter($db);
if (!is_array($saved)) { throw new RuntimeException('Исходный режим неизвестен'); }
if (!cubrid_set_db_parameter($db, CUBRID_PARAM_LOCK_TIMEOUT, 1000)) {
throw new RuntimeException('Не задан лимит ожидания');
}
try {
// Выполнить короткую операцию; при отказе сохранить ошибку и сделать rollback.
} finally {
if (!cubrid_set_db_parameter($db, CUBRID_PARAM_LOCK_TIMEOUT, $saved['PARAM_LOCK_TIMEOUT'])) {
throw new RuntimeException('Не восстановлена настройка соединения');
}
}Резервирование занятого товара показывает весь жизненный цикл, включая владельца блокировки и успешный новый резерв после отмены. Восстановление настройки не равно COMMIT или ROLLBACK: управление изменениями остаётся отдельной обязанностью приложения.
Полная историческая шкала PHP Manual и современный профиль
| Уровень старого API | Смысл исторического описания |
|---|---|
| 1: READ COMMITTED CLASS / READ UNCOMMITTED INSTANCES | Повторное описание схемы может измениться; возможны неподтверждённые значения строк. |
| 2: READ COMMITTED CLASS / READ COMMITTED INSTANCES | Возможны изменения повторного чтения схемы и подтверждённых строк. |
| 3: REPEATABLE READ CLASS / READ UNCOMMITTED INSTANCES | Схема защищена от изменения, но возможны неподтверждённые строки; старая страница называет этот уровень исходным. |
| 4: REPEATABLE READ CLASS / READ COMMITTED INSTANCES | Схема повторяема, подтверждённая строка может измениться между чтениями; также CURSOR STABILITY. |
| 5: REPEATABLE READ CLASS / REPEATABLE READ INSTANCES | Защищено повторное чтение строки; историческая таблица отдельно предупреждает о фантомных вставках. |
| 6: SERIALIZABLE | Исторический контракт максимальной изоляции описывает отсутствие грязных, неповторяемых и фантомных чтений. |
Это сохранённая историческая шкала, а не обещание, что современный сервер принимает все шесть режимов и имеет исходный уровень 3. Современная документация CUBRID использует уровни 4/5/6 и отмечает ограничения доступности 6 в зависимости от версии. В данном профиле исходно наблюдён уровень 4; рабочее сравнение изолирует уровни 4 и 5. Номер без версии сервера недостаточен для выводов о конкурентном поведении.
Getter не доказывает, что каждое SQL-чтение всегда вернёт одинаковые значения: для этого важны реальная граница транзакции, режим и чужие COMMIT. Повторно используемое соединение может сохранить изменённые настройки; считывание фактического состояния помогает обнаружить это до полезной операции.
cubrid_set_db_parameterЗадать соединению ожидание блокировки либо изоляцию следующей операции.
PECL CUBRID >= 8.4.0 · cubrid
cubrid_set_db_parameter(resource $conn_identifier, int $param_type, int $param_value): boolЗадать соединению ожидание блокировки либо изоляцию следующей операции.
cubrid_set_db_parameter(resource $conn_identifier, int $param_type, int $param_value): boolЭто запись справочного контракта. Обозначения resource, int, bool и array описывают значения; строку с resource не нужно копировать как собственное объявление PHP-функции. Версия появления: PECL CUBRID >= 8.4.0. Рабочие программы используют отдельно закреплённые PHP 7.4.33 и native-драйвер 11.2.0.0002.
| Параметр | Смысл и границы |
|---|---|
| conn_identifier | Конкретное живое соединение. Сигнатура и закреплённый код требуют этот аргумент; историческую фразу страницы о last link при пропуске нельзя использовать как переносимый вызов. |
| param_type | Только CUBRID_PARAM_ISOLATION_LEVEL либо CUBRID_PARAM_LOCK_TIMEOUT. Другие читаемые параметры getter не становятся автоматически записываемыми. |
| param_value | Целый номер изоляции либо предел ожидания блокировки. В данном CCI 11.2 для lock это миллисекунды; историческая документация PHP описывает секунды старого CCI до 9.0. |
Возвращаемое значение и отказ
true означает, что изменение принято; false — отказ с диагностикой. После успеха cubrid_get_db_parameter позволяет прочитать фактическое состояние. Историческое «значение изоляции 1–6» сверяйте с доступными режимами конкретной версии. В подготовке неизвестный тип параметра вернул false/-30022/facility4; ошибка не исправляется увеличением тайм-аута.
Применение 1: ограничить ожидание занятого остатка
$ok = cubrid_set_db_parameter($db, CUBRID_PARAM_LOCK_TIMEOUT, 1000);
if (!$ok) {
$code = cubrid_error_code(); // До любых следующих вызовов драйвера.
throw new RuntimeException('Не задано ожидание, code='.$code);
}
$params = cubrid_get_db_parameter($db);
echo $params['PARAM_LOCK_TIMEOUT']; // 1000 миллисекунд в этом профилеПолный конфликт двух сессий не подменяет ожидание искусственной задержкой: другой процесс действительно держит изменённую строку. В этом профиле lock 1000 мс при query 5000 мс привёл к отказу -75/facility 1. Приложение явно отменяет операцию, владелец отпускает строку, затем новая команда сохраняет остаток 2. Этот код привязан к наблюдённому отказу, а не ко всем возможным проблемам UPDATE.
| Lock timeout | Политика | Когда осмысленно |
|---|---|---|
| 0 | Не ждать занятую блокировку. | Быстро предложить пользователю обновить карточку или повторить действие позже. |
| Положительное число | Предел в миллисекундах в CCI 9.0+; 1000 соответствует 1 секунде. | Разрешить краткий конфликт без долгого удержания HTTP-запроса. |
| -1 | Бессрочное ожидание в рамках этой настройки. | Не является безопасным общим выбором для интерактивного экрана; нужны иные контролируемые пределы. |
Это политика соединения, а не команда прервать чужую транзакцию. Увеличенный срок не лечит потерянный COMMIT, конфликтующий порядок захвата нескольких строк или отсутствие нужного индекса. Deadlock и исчерпание срока — разные события; решение по ошибке принимает приложение.
Не смешивайте единицы разных поколений: исторические секунды PHP Manual относятся к старому CCI. В закреплённом CCI 11.2 и сервере 11.3.5 передают 1000 для одной секунды. Полная граница и источник приведены у cubrid_get_db_parameter. Значения 0 и -1 — специальные режимы, а не длительности 0 и -1 миллисекунд.
Применение 2: выбрать повторяемость чтения для отчёта
// Перед этим закончите предыдущую полезную операцию commit или rollback.
if (!cubrid_set_db_parameter($db, CUBRID_PARAM_ISOLATION_LEVEL, 5)) {
throw new RuntimeException('REPEATABLE READ не принят');
}
$first = cubrid_execute($db, 'SELECT title,qty FROM products WHERE id=1');
// Прочитать и закрыть request. Другой клиент может сделать свой COMMIT.
// Следующее чтение в этой транзакции проверяют по правилам выбранной изоляции.
// По завершении отчёта явно завершить транзакцию.Сравнение двух читателей показывает значения 3 и 7 до и после чужого подтверждения. Режим задаётся до первого значимого чтения операции. CUBRID допускает изменение режима во время транзакции, однако это не делает предшествующие чтения задним числом повторяемыми. Для понятного отчёта устанавливайте режим на границе транзакции.
Для autocommit существует cubrid_set_autocommit, для request — cubrid_set_query_timeout. Подставлять их имена в param_type нельзя. Не менять глобальный cubrid.conf ради одного экрана: настройки конкретного соединения имеют меньшую область воздействия. Старые уровни и современные ограничения подробно разобраны в cubrid_get_db_parameter.
cubrid_get_query_timeoutПроверить установленный бюджет конкретного request в миллисекундах.
PECL CUBRID >= 8.4.1 · cubrid
cubrid_get_query_timeout(resource $req_identifier): int|falseПроверить установленный бюджет конкретного request в миллисекундах.
cubrid_get_query_timeout(resource $req_identifier): int|falseЭто запись справочного контракта. Обозначения resource, int, bool и array описывают значения; строку с resource не нужно копировать как собственное объявление PHP-функции. Версия появления: PECL CUBRID >= 8.4.1. Рабочие программы используют отдельно закреплённые PHP 7.4.33 и native-драйвер 11.2.0.0002.
| Параметр | Смысл и границы |
|---|---|
| req_identifier | Живой ресурс отдельного запроса после cubrid_prepare. Это не connection, PID процесса и не время HTTP-запроса. |
Возвращаемое значение и отказ
Целое значение в миллисекундах либо false при отказе. Сравнение должно быть строгим: 0 — числовая настройка, а не false. Функция читает конфигурацию request; она не запускает SQL, не сообщает прошедшее время и не подтверждает, что запрос успел выполнить полезную работу.
Применение 1: проверить независимые бюджеты экрана и отчёта
$screenMs = cubrid_get_query_timeout($screen);
$reportMs = cubrid_get_query_timeout($report);
if ($screenMs === false || $reportMs === false) {
throw new RuntimeException('Бюджеты неизвестны');
}
echo 'Экран: ', $screenMs, ' мс; отчёт: ', $reportMs, ' мс';В проекте с двумя SELECT прочитаны 250 и 1500 мс. После изменения screen на 400 мс report сохраняет 1500 мс. Сам отчёт возвращает 2 позиции и 10 единиц, а не только сообщение «настройка верна». Для теста бюджета полезный результат проверяют отдельно от getter.
Исходный query timeout может приходить из свойств подключения. PHP Manual показывает URL-параметр query_timeout=5000 и getter подготовленного запроса со значением 5000. Установленная через setter величина переопределяет конкретный request; это не настройка всех новых запросов текущего сайта. login_timeout и disconnect_on_query_timeout — отдельные свойства соединения со своим назначением.
Применение 2: сверить политику перед диагностируемым ожиданием
$queryMs = cubrid_get_query_timeout($request);
$params = cubrid_get_db_parameter($db);
if ($queryMs === false || !is_array($params)) {
throw new RuntimeException('Неизвестны пределы ожидания');
}
echo 'Команда: ', $queryMs, ' мс; блокировка: ', $params['PARAM_LOCK_TIMEOUT'], ' мс (CCI 11.2)';
// Только затем execute и немедленное сохранение его ошибки при отказе.Операция с предварительной пометкой использует 200 мс для request и 5 секунд для блокировки. Время, измеренное microtime вокруг execute, сохраняется как наблюдение; оно не заменяет чтение конфигурации и не сравнивается с точным числом миллисекунд. Внешняя нагрузка, планировщик и обмен с сервером влияют на измерение.
Ноль означает отсутствие конечного query deadline, заданного этой настройкой; он не отменяет другие ограничения сети, блокировки или сервера. В данном CCI таймер включается только при положительном current_timeout, поэтому getter 0 нельзя проверять через if (!$timeout). Значение -1 не является аналогом бесконечного lock timeout: отрицательный запрос setter в подготовке отклонён, сохранённое значение осталось 0. Для интерактивной операции выбирают явный положительный бюджет.
Если execute уже вернул false, сначала сохраните code/facility/message, затем вызывайте getter и другие функции. Иначе диагностируется последний getter, а не первоначально неудачная команда. Закрытый request нельзя использовать для чтения бюджета; его владение заканчивается в cubrid_close_request.
cubrid_set_query_timeoutОграничить допустимое исполнение выбранного request, не меняя автоматически соседние запросы.
PECL CUBRID >= 8.4.1 · cubrid
cubrid_set_query_timeout(resource $req_identifier, int $timeout): boolОграничить допустимое исполнение выбранного request, не меняя автоматически соседние запросы.
cubrid_set_query_timeout(resource $req_identifier, int $timeout): boolЭто запись справочного контракта. Обозначения resource, int, bool и array описывают значения; строку с resource не нужно копировать как собственное объявление PHP-функции. Версия появления: PECL CUBRID >= 8.4.1. Рабочие программы используют отдельно закреплённые PHP 7.4.33 и native-драйвер 11.2.0.0002.
| Параметр | Смысл и границы |
|---|---|
| req_identifier | Ресурс той подготовленной команды, которой нужно назначить бюджет. Устанавливайте перед execute, пока request жив. |
| timeout | Целое число миллисекунд. 1000 мс = 1 с. Не переносите сюда -1 из lock timeout; подготовка показала его отказ. |
Возвращаемое значение и отказ
true означает принятую настройку, false — ошибку. Это не результат самого SQL и не отложенное обещание успеха. Нормативный PHP-контракт описывает время выполнения; реальное поведение отмены и соединения зависит также от CCI, сервера и свойств подключения.
Применение 1: короткий бюджет экранной выборки
$request = cubrid_prepare($db, 'SELECT title,qty FROM products WHERE qty>0 ORDER BY id LIMIT 2');
if ($request === false) { throw new RuntimeException('Нет request'); }
try {
if (!cubrid_set_query_timeout($request, 250)) {
throw new RuntimeException('Не задан бюджет');
}
if (!cubrid_execute($request)) {
// Сохранить code/facility/message прежде, чем выполнить rollback или другой вызов.
throw new RuntimeException('Экранная выборка не получена');
}
// Прочитать выбранные строки и завершить транзакцию согласно операции.
} finally {
cubrid_close_request($request);
}Полностью обработанный вариант находится в экране и отчёте. LIMIT ограничивает число строк, WHERE — нужные данные, явный список колонок — формат результата, timeout — допустимое ожидание исполнения. Ни один из этих инструментов не заменяет остальные. Увеличивать timeout после каждой неудачи без анализа причины не значит оптимизировать запрос.
Применение 2: прекратить составную операцию после отказа команды
if (!cubrid_set_query_timeout($request, 200)) {
throw new RuntimeException('Не задан предел команды');
}
$ok = cubrid_execute($request);
if ($ok === false) {
$code = cubrid_error_code();
$facility = cubrid_error_code_facility();
$message = cubrid_error_msg();
if (!cubrid_rollback($db)) {
throw new RuntimeException('Результат отмены неизвестен');
}
// Пользователю: операция не завершена. Закрытый журнал: code/facility,
// без публикации полного текста, в котором могут быть служебные пути и SQL.
}Полный проект с предварительным UPDATE подтверждает возвращение второй позиции к примечанию «обычный», первой — к остатку 3. Лишь после освобождения блокировки выполняется отдельное новое резервирование до 2. Код не продолжает частичную бизнес-операцию под видом успешной.
Setter не вызывает задержку на заданное время: быстрый запрос заканчивается раньше. Он не является механизмом фоновой отмены уже работающего синхронного PHP-вызова и не ограничивает весь HTTP-запрос. В лаборатории общий срок жизни процесса ограничен отдельно, чтобы сбой среды не оставлял тестовый владелец блокировки работать бесконечно.
При неверном отрицательном timeout подготовка наблюдала false/-20043/facility3 и неизменённый прежний getter. Для исправления конфигурации выбирают допустимую величину, а не повторяют полезный UPDATE. При сетевом обрыве результат подтверждения может быть неизвестен: повтор операции требует отдельной идемпотентности или проверки её идентификатора, а не просто увеличения числа миллисекунд.
В контролируемом конфликте lock 5000 мс / query 200 мс сервер вернул -4/facility 1 — прерывание команды при сработавшем request deadline. Сам по себе код -4 не доказывает, что причиной любого отказа был именно этот таймер. Здесь причина связана с установленным бюджетом, удерживаемой строкой и наблюдением двух настоящих сессий; elapsed сохраняется отдельно, без обещания ровно 200 мс.
Исчерпание срока инициирует отмену запроса; контракт подключения предупреждает, что фактический исход зависит от disconnect_on_query_timeout и команда всё ещё может завершиться успешно. Поэтому проверяют реальный return, сразу сохраняют диагностику и учитывают бизнес-результат. Нельзя объявить операцию отменённой только потому, что часы показали больше заданного числа.
Источники и обсуждения
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


