fix(projects): advisory lock и ошибка переполнения счётчика technical_number в add_project (AURORA-1831)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2a287480b6
commit
95b74bc5e5
15
api/alembic/versions/0050_project_technical_number_lock.py
Normal file
15
api/alembic/versions/0050_project_technical_number_lock.py
Normal file
@ -0,0 +1,15 @@
|
||||
from sql_migration import run_sql_file_migration
|
||||
|
||||
|
||||
revision = "0050"
|
||||
down_revision = "0049"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
run_sql_file_migration("0050_project_technical_number_lock.sql")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@ -0,0 +1,96 @@
|
||||
-- AURORA-1831: гонка technical_number при параллельном создании проектов.
|
||||
-- v3.add_project: копия версии из 0018 + advisory lock перед вычислением счётчика
|
||||
-- и понятная ошибка при переполнении счётчика (больше 99 проектов на оргединицу).
|
||||
|
||||
CREATE OR REPLACE FUNCTION v3.add_project(p_name character varying, p_year integer, p_org_unit_id integer, p_level character varying DEFAULT 'project'::character varying, p_parent_id integer DEFAULT NULL::integer, p_project_type character varying DEFAULT NULL::character varying, p_vsp_format character varying DEFAULT NULL::character varying, p_placement_type character varying DEFAULT NULL::character varying, p_object_address character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric, p_system_code character varying DEFAULT NULL::character varying)
|
||||
RETURNS TABLE(project_id integer, limit_report_id integer, current_expenses_report_id integer)
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_pid INT;
|
||||
v_lim INT;
|
||||
v_cur INT;
|
||||
v_seq INT;
|
||||
v_technical_number VARCHAR;
|
||||
BEGIN
|
||||
IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
|
||||
RAISE EXCEPTION 'p_name обязателен';
|
||||
END IF;
|
||||
IF p_year IS NULL THEN
|
||||
RAISE EXCEPTION 'p_year обязателен';
|
||||
END IF;
|
||||
IF p_org_unit_id IS NULL THEN
|
||||
RAISE EXCEPTION 'p_org_unit_id обязателен';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM v3.org_unit ou WHERE id = p_org_unit_id AND ou.is_active) THEN
|
||||
RAISE EXCEPTION 'org_unit id=% не существует', p_org_unit_id;
|
||||
END IF;
|
||||
IF p_parent_id IS NOT NULL AND
|
||||
NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_parent_id AND level = 'program') THEN
|
||||
RAISE EXCEPTION 'parent project id=% не существует или не имеет level=program', p_parent_id;
|
||||
END IF;
|
||||
|
||||
-- AURORA-1831: сериализуем выдачу номера внутри одной оргединицы. Без блокировки
|
||||
-- две параллельные транзакции читают один и тот же MAX и падают на уникальном индексе.
|
||||
-- Блокировка снимается на COMMIT/ROLLBACK. Решение опирается на READ COMMITTED
|
||||
-- (приложение не меняет изоляцию): после получения блокировки SELECT MAX берёт новый
|
||||
-- снимок и видит строку, закоммиченную предыдущим владельцем. При REPEATABLE READ
|
||||
-- и выше этого недостаточно.
|
||||
PERFORM pg_advisory_xact_lock(hashtext('v3.project.technical_number'), p_org_unit_id);
|
||||
|
||||
-- Порядковый счётчик проекта для данного РФ/ВСП (org_unit)
|
||||
SELECT COALESCE(MAX(right(technical_number, 2)::int), 0) + 1
|
||||
INTO v_seq
|
||||
FROM v3.project
|
||||
WHERE org_unit_id = p_org_unit_id
|
||||
AND technical_number IS NOT NULL;
|
||||
|
||||
IF v_seq > 99 THEN
|
||||
RAISE EXCEPTION 'Исчерпан счётчик технических номеров проектов для org_unit id=% (максимум 99)', p_org_unit_id;
|
||||
END IF;
|
||||
|
||||
v_technical_number := to_char(now(), 'YYMM') || '_'
|
||||
|| lpad(p_org_unit_id::text, 4, '0')
|
||||
|| lpad(v_seq::text, 2, '0');
|
||||
|
||||
INSERT INTO v3.project (
|
||||
name, level, parent_id, org_unit_id,
|
||||
project_type, vsp_format, placement_type, object_address, staff_count, total_area,
|
||||
system_code, technical_number
|
||||
) VALUES (
|
||||
p_name, p_level, p_parent_id, p_org_unit_id,
|
||||
p_project_type, p_vsp_format, p_placement_type, p_object_address, p_staff_count, p_total_area,
|
||||
p_system_code, v_technical_number
|
||||
) RETURNING id INTO v_pid;
|
||||
|
||||
-- Первый год: 2 отчёта (LIMIT + CURRENT_EXPENSES) + snapshot фаз + сетка статей.
|
||||
SELECT y.limit_report_id, y.current_expenses_report_id
|
||||
INTO v_lim, v_cur
|
||||
FROM v3.add_project_year(v_pid, p_year, false) y;
|
||||
|
||||
-- Аудит: PROJECT_CREATE — все заполненные поля + id отчётов
|
||||
PERFORM v3.log_event(
|
||||
'PROJECT_CREATE', 'PROJECT',
|
||||
jsonb_strip_nulls(jsonb_build_object(
|
||||
'project_id', v_pid,
|
||||
'name', p_name,
|
||||
'year', p_year,
|
||||
'level', p_level,
|
||||
'parent_id', p_parent_id,
|
||||
'org_unit_id', p_org_unit_id,
|
||||
'project_type', p_project_type,
|
||||
'vsp_format', p_vsp_format,
|
||||
'placement_type', p_placement_type,
|
||||
'object_address', p_object_address,
|
||||
'system_code', p_system_code,
|
||||
'technical_number', v_technical_number,
|
||||
'staff_count', p_staff_count,
|
||||
'total_area', p_total_area,
|
||||
'limit_report_id', v_lim,
|
||||
'current_expenses_report_id', v_cur
|
||||
))
|
||||
);
|
||||
|
||||
RETURN QUERY SELECT v_pid, v_lim, v_cur;
|
||||
END;
|
||||
$function$;
|
||||
Loading…
x
Reference in New Issue
Block a user