letopis 1.4.1 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/sql/90-time.sql CHANGED
@@ -389,9 +389,12 @@ begin
389
389
  end if;
390
390
  end loop;
391
391
  end if;
392
+ -- по классам: последняя версия id — любого класса, класс проверяется у неё (план 2.0, 7.1 №1)
392
393
  for r in select distinct on (l.id) l.* from @SCHEMA@.log l
393
- where l.tenant = c.tenant and (case when p_ids is not null then l.id = any (p_ids) else l.class = any (p_classes) end)
394
+ where l.tenant = c.tenant and (case when p_ids is not null then l.id = any (p_ids)
395
+ else l.id in (select x.id from @SCHEMA@.log x where x.tenant = c.tenant and x.class = any (p_classes)) end)
394
396
  order by l.id, l.rev desc loop
397
+ continue when p_ids is null and not (r.class = any (p_classes));
395
398
  continue when r.op in ('purge', 'reset')
396
399
  or not @SCHEMA@.row_visible(r.tenant, r.class, r.id, r.owner, r.tags, r.data, r.links, c.tenant, c.account, rd->'plan');
397
400
  if r.class in ('Class', 'Account', 'member', 'Resource', 'rule') then
@@ -797,7 +800,9 @@ end $$;
797
800
  -- ========================= обслуживание =========================
798
801
 
799
802
  -- Одна пачка прореживания журнала (§2.11): версии, которые не нужны действующей политике хранения
800
- -- своего класса. Не трогаются версия 1, текущая и версии особых видов (create, delete, reclass,
803
+ -- своего класса — как его резолвит class_row: свой класс арендатора, иначе System; у арендатора со своим одноимённым
804
+ -- классом политика класса System не действует (план 2.0, 7.3 №1: класс System, заведённый позже, дублировал строки
805
+ -- журнала в отборе и стирал промежуточные версии). Не трогаются версия 1, текущая и версии особых видов (create, delete, reclass,
801
806
  -- rekey, restore, purge, reset, trim); история удалённого объекта, надгробие которого старше
802
807
  -- последнего яруса со сроком, стирается целиком. Пачка — до 100 объектов после курсора (meta
803
808
  -- строки ключа: участки по порядку id), не больше p_limit версий; объект, не вошедший целиком,
@@ -827,32 +832,60 @@ begin
827
832
  )
828
833
  select array_agg(q.id order by q.id) into ids from (
829
834
  select distinct l.id from @SCHEMA@.log l
830
- join pol on pol.p is not null and pol.cls = l.class and (pol.ct = l.tenant or pol.ct = '@SYSTEM@'::uuid)
835
+ join pol on pol.p is not null and pol.cls = l.class
836
+ and (pol.ct = l.tenant
837
+ or pol.ct = '@SYSTEM@'::uuid
838
+ and not exists (select 1 from @SCHEMA@.entity y
839
+ where y.class = 'Class' and y.tenant = l.tenant and y.data->>'name' = l.class))
831
840
  where l.at < p_now - @SCHEMA@.history_iv(pol.p->>'all') and (cur is null or l.id > cur)
832
841
  order by l.id limit 100) q;
833
842
  if ids is null then
834
843
  update @SCHEMA@.secret s set meta = s.meta - 'thin' where s.id = '@SYSTEM@'::uuid;
835
844
  return jsonb_build_object('thinned', 0, 'more', false);
836
845
  end if;
846
+ -- последняя запись каждого id пачки — for update: restore(), purge() и новая версия того же id берут её же (инвариант
847
+ -- M-TIME); отбор ниже видит то, что они успели зафиксировать, и не стирает историю объекта, который только что
848
+ -- восстановили (план 2.0, 7.3 №3)
849
+ perform 1 from @SCHEMA@.log l
850
+ where l.id = any (ids) and not exists (select 1 from @SCHEMA@.log z where z.id = l.id and z.rev > l.rev)
851
+ order by l.id
852
+ for update of l;
837
853
  with pol as (
838
854
  select x.data->>'name' as cls, x.tenant as ct, @SCHEMA@.history_policy(x.data->'resolved') as p
839
855
  from @SCHEMA@.entity x where x.class = 'Class'
840
856
  ), v as (
841
- select l.id, l.rev, l.at, l.op, pol.p, coalesce(pol.p->>'tz', 'UTC') as tz,
857
+ -- все версии id, любого класса: последняя версия и следующая по времени — по всей цепочке (план 2.0, 7.3 №2: после
858
+ -- reclass версии прежнего класса без политики выпадали из отбора и переживали стирание истории удалённого)
859
+ select l.id, l.rev, l.at, l.op, l.class, l.tenant,
842
860
  max(l.rev) over (partition by l.id) as last_rev,
843
861
  first_value(l.op) over (partition by l.id order by l.rev desc) as last_op,
844
862
  first_value(l.at) over (partition by l.id order by l.rev desc) as last_at,
863
+ first_value(l.class) over (partition by l.id order by l.rev desc) as last_class,
845
864
  lead(l.at) over (partition by l.id order by l.rev) as next_at
846
865
  from @SCHEMA@.log l
847
- join pol on pol.p is not null and pol.cls = l.class and (pol.ct = l.tenant or pol.ct = '@SYSTEM@'::uuid)
848
866
  where l.id = any (ids)
867
+ ), vp as (
868
+ -- прореживание версии — по политике её класса; стирание удалённого целиком — по политике класса надгробия
869
+ select v.*, pv.p, coalesce(pv.p->>'tz', 'UTC') as tz, pl.p as lp, coalesce(pl.p->>'tz', 'UTC') as ltz
870
+ from v
871
+ left join pol pv on pv.cls = v.class
872
+ and (pv.ct = v.tenant
873
+ or pv.ct = '@SYSTEM@'::uuid
874
+ and not exists (select 1 from @SCHEMA@.entity y
875
+ where y.class = 'Class' and y.tenant = v.tenant and y.data->>'name' = v.class))
876
+ left join pol pl on pl.cls = v.last_class
877
+ and (pl.ct = v.tenant
878
+ or pl.ct = '@SYSTEM@'::uuid
879
+ and not exists (select 1 from @SCHEMA@.entity y
880
+ where y.class = 'Class' and y.tenant = v.tenant and y.data->>'name' = v.last_class))
849
881
  )
850
- select array_agg(v.id order by v.id, v.rev), array_agg(v.rev order by v.id, v.rev) into vid, vrev
851
- from v
852
- where (v.last_op = 'delete' and timezone(v.tz, v.last_at) <= timezone(v.tz, p_now) - @SCHEMA@.history_horizon(v.p))
853
- or (v.rev <> 1 and v.rev <> v.last_rev
854
- and v.op not in ('create', 'delete', 'reclass', 'rekey', 'restore', 'purge', 'reset', 'trim')
855
- and not @SCHEMA@.history_keep(v.p, timezone(v.tz, v.at), timezone(v.tz, p_now), timezone(v.tz, v.next_at)));
882
+ select array_agg(vp.id order by vp.id, vp.rev), array_agg(vp.rev order by vp.id, vp.rev) into vid, vrev
883
+ from vp
884
+ where (vp.last_op = 'delete' and vp.lp is not null
885
+ and timezone(vp.ltz, vp.last_at) <= timezone(vp.ltz, p_now) - @SCHEMA@.history_horizon(vp.lp))
886
+ or (vp.p is not null and vp.rev <> 1 and vp.rev <> vp.last_rev
887
+ and vp.op not in ('create', 'delete', 'reclass', 'rekey', 'restore', 'purge', 'reset', 'trim')
888
+ and not @SCHEMA@.history_keep(vp.p, timezone(vp.tz, vp.at), timezone(vp.tz, p_now), timezone(vp.tz, vp.next_at)));
856
889
  total := coalesce(cardinality(vid), 0);
857
890
  k := least(total, p_limit);
858
891
  if k > 0 then
@@ -875,11 +908,15 @@ begin
875
908
  return jsonb_build_object('thinned', k, 'more', more);
876
909
  end $$;
877
910
 
878
- -- Обслуживание (этап 6, п. 8): истёкшие и погашенные сессии и одноразовые коды, одна пачка
879
- -- прореживания журнала, заполнение очереди сигналов. Вызывает системный администратор, роль с
911
+ -- Обслуживание (этап 6, п. 8): истёкшие и погашенные сессии и одноразовые коды, строки-замки удалённых семейств, одна
912
+ -- пачка прореживания журнала, заполнение очереди сигналов. Вызывает системный администратор, роль с
880
913
  -- правом letopis.maintain или администраторское подключение. p_now — момент «сейчас» для
881
- -- прореживания, только при allow_reset (тесты): иначе одним вызовом можно было бы проредить всю историю.
882
- create or replace function @SCHEMA@.maintain(p_now timestamptz default null, p_limit int default 10000) returns jsonb
914
+ -- прореживания, только при allow_reset (тесты): иначе одним вызовом можно было бы проредить всю историю. p_cleanup —
915
+ -- удалять ли истёкшие сессии, коды и строки-замки: библиотека передаёт true только в первой пачке вызова, иначе каждая
916
+ -- из ≤100 пачек заново просматривала бы secret (план 2.0, 7.3 №4); прежний вызов без третьего аргумента чистит, как раньше.
917
+ drop function if exists @SCHEMA@.maintain(timestamptz, int);
918
+ create or replace function @SCHEMA@.maintain(p_now timestamptz default null, p_limit int default 10000,
919
+ p_cleanup boolean default true) returns jsonb
883
920
  language plpgsql volatile security definer
884
921
  set search_path = pg_catalog, @SCHEMA@, pg_temp
885
922
  as $$
@@ -887,6 +924,7 @@ declare
887
924
  ts timestamptz := clock_timestamp();
888
925
  s int;
889
926
  o int;
927
+ fam text := '@SCHEMA_NAME@:family:';
890
928
  begin
891
929
  if not @SCHEMA@.letopis_allowed('letopis.maintain') then
892
930
  perform @SCHEMA@.lt_fail('acl_denied', 'нет права letopis.maintain');
@@ -902,14 +940,41 @@ begin
902
940
  end if;
903
941
  ts := p_now;
904
942
  end if;
905
- with d as (delete from @SCHEMA@.secret x where x.kind = 'SESSION'
906
- and (x.deleted_at is not null or x.expires_at <= clock_timestamp()) returning 1)
907
- select count(*)::int into s from d;
908
- -- коды всех видов, которые выпускает auth_issue_otp: OTP и OTP_<имя> (тот же шаблон вида), — использованные,
909
- -- заменённые, исчерпавшие попытки (deleted_at) и истёкшие
910
- with d as (delete from @SCHEMA@.secret x where x.kind ~ '^OTP(_[A-Z0-9]{1,27})?$'
911
- and (x.deleted_at is not null or x.expires_at <= clock_timestamp()) returning 1)
912
- select count(*)::int into o from d;
943
+ s := 0;
944
+ o := 0;
945
+ if coalesce(p_cleanup, true) then
946
+ with d as (delete from @SCHEMA@.secret x where x.kind = 'SESSION'
947
+ and (x.deleted_at is not null or x.expires_at <= clock_timestamp()) returning 1)
948
+ select count(*)::int into s from d;
949
+ -- коды всех видов, которые выпускает auth_issue_otp: OTP и OTP_<имя> (тот же шаблон вида), — использованные,
950
+ -- заменённые, исчерпавшие попытки (deleted_at) и истёкшие
951
+ with d as (delete from @SCHEMA@.secret x where x.kind ~ '^OTP(_[A-Z0-9]{1,27})?$'
952
+ and (x.deleted_at is not null or x.expires_at <= clock_timestamp()) returning 1)
953
+ select count(*)::int into o from d;
954
+ -- строки-замки семейств, чьего корня больше нет — удалён класс или его арендатор (план 2.0, 7.7 №8, долгий
955
+ -- прогон): их заранее заводит запись строки Class (family_rows), а сами они не уходили — заведение и удаление
956
+ -- классов копило их без предела. Корень жив — строка Class с этим именем у арендатора семейства (family_name;
957
+ -- в имени класса двоеточия нет). Занятую строку не трогаем (skip locked), а строку-замок семейства — только вместе
958
+ -- с её шлюзом или без шлюза: исключительная блокировка берёт шлюз первым, и строка-замок, до которой она ещё не
959
+ -- дошла, у неё не пропадёт. Пропавшую строку lock_row и family_lock заводят заново, если класс с этим именем
960
+ -- появится снова.
961
+ with stale as (
962
+ select l.id, l.meta->>'name' as n
963
+ from @SCHEMA@.secret l
964
+ where l.kind = 'LOCK' and l.account = '@SYSTEM@'::uuid and l.deleted_at is null
965
+ and left(l.meta->>'name', length(fam)) = fam
966
+ and not exists (
967
+ select 1 from @SCHEMA@.entity c
968
+ where c.class = 'Class'
969
+ and c.data->>'name' = split_part(substr(l.meta->>'name', length(fam) + 1), ':', 2)
970
+ and c.tenant::text = split_part(substr(l.meta->>'name', length(fam) + 1), ':', 1))
971
+ for update of l skip locked)
972
+ delete from @SCHEMA@.secret x using stale st
973
+ where x.id = st.id
974
+ and (st.n like '%:gate'
975
+ or exists (select 1 from stale g where g.n = st.n || ':gate')
976
+ or not exists (select 1 from @SCHEMA@.secret y where y.id = @SCHEMA@.lock_id(st.n || ':gate')));
977
+ end if;
913
978
  return jsonb_build_object('sessions', s, 'codes', o, 'queue', pg_notification_queue_usage())
914
979
  || @SCHEMA@.thin_batch(ts, greatest(coalesce(p_limit, 10000), 1));
915
980
  end $$;
@@ -969,8 +1034,12 @@ begin
969
1034
  select jsonb_build_object('id', id, 'rev', want_rev, 'problem', 'разрыв: нет версий ' || want_rev || '…' || (rev - 1))
970
1035
  from exp
971
1036
  where rev > want_rev
972
- and not exists (select 1 from pol where pol.cls = exp.class and (pol.ct = exp.tenant or pol.ct = '@SYSTEM@'::uuid)
973
- and exp.prev_at < ts - pol.all_iv)
1037
+ and not exists (select 1 from pol where pol.cls = exp.class and exp.prev_at < ts - pol.all_iv
1038
+ and (pol.ct = exp.tenant
1039
+ or pol.ct = '@SYSTEM@'::uuid
1040
+ and not exists (select 1 from @SCHEMA@.entity y
1041
+ where y.class = 'Class' and y.tenant = exp.tenant
1042
+ and y.data->>'name' = exp.class)))
974
1043
  union all
975
1044
  select jsonb_build_object('id', id, 'rev', rev, 'problem', 'номер версии меньше ожидаемого после обрезки')
976
1045
  from exp where rev < want_rev
@@ -1064,11 +1133,20 @@ as $$
1064
1133
  declare
1065
1134
  horizon xid8 := pg_snapshot_xmin(pg_current_snapshot());
1066
1135
  lim interval;
1136
+ t uuid := @SCHEMA@.ctx_tenant();
1067
1137
  begin
1068
1138
  if p_at is not null then
1139
+ -- ярус all классов, которые видит подписка: арендатора сессии и System (кроме одноимённых своих); политика класса
1140
+ -- чужого арендатора с тем же именем курсор не просрочивает (план 2.0, 7.3 №1). Без арендатора в контексте
1141
+ -- (администраторское подключение видит всех) — все классы
1069
1142
  select min(@SCHEMA@.history_iv(p.p->>'all')) into lim
1070
1143
  from @SCHEMA@.entity x cross join lateral (select @SCHEMA@.history_policy(x.data->'resolved') as p) p
1071
- where x.class = 'Class' and p.p is not null and (p_classes is null or x.data->>'name' = any (p_classes));
1144
+ where x.class = 'Class' and p.p is not null and (p_classes is null or x.data->>'name' = any (p_classes))
1145
+ and (t is null
1146
+ or x.tenant = t
1147
+ or x.tenant = '@SYSTEM@'::uuid
1148
+ and not exists (select 1 from @SCHEMA@.entity y
1149
+ where y.class = 'Class' and y.tenant = t and y.data->>'name' = x.data->>'name'));
1072
1150
  if lim is not null and p_at < clock_timestamp() - lim then
1073
1151
  raise exception using errcode = 'LT016',
1074
1152
  message = 'letopis: cursor_expired: курсор подписки старше яруса all — события могли проредиться, перечитайте текущее состояние';
@@ -1156,7 +1234,7 @@ begin
1156
1234
  'history_keep(jsonb, timestamp, timestamp, timestamp)', 'session_role()', 'admin_role()', 'letopis_allowed(text)', 'session_expires()', 'signal_mode()', 'signal_key()', 'class_stamp()',
1157
1235
  'last_version_visible(uuid, uuid, jsonb)', 'restore(uuid)', 'projector_unread(uuid, uuid, int)', 'purge(uuid, boolean)', 'trim_history(uuid[], text[], timestamptz)',
1158
1236
  'rekey(uuid, jsonb, jsonb)', 'rekey_class(text, jsonb)', 'account_purge(uuid)', 'reset_mark(uuid, text, uuid)',
1159
- 'reset(text, text, uuid, int)', 'thin_batch(timestamptz, int)', 'maintain(timestamptz, int)', 'verify(boolean)',
1237
+ 'reset(text, text, uuid, int)', 'thin_batch(timestamptz, int)', 'maintain(timestamptz, int, boolean)', 'verify(boolean)',
1160
1238
  'watch_read(xid8, bigint, timestamptz, text[], int, pg_snapshot, boolean)', 'watch_leaving(xid8, bigint, text[], int, pg_snapshot, xid8, bigint)', 'watch_head()'] loop
1161
1239
  execute format('alter function @SCHEMA@.%s owner to @OWNER@', f);
1162
1240
  execute format('revoke all on function @SCHEMA@.%s from public', f);
@@ -1165,7 +1243,7 @@ end $$;
1165
1243
  -- приложению: функции владельца этапа 6 (проверяют права сами), подписка (под RLS) и чистые помощники политики
1166
1244
  grant execute on function @SCHEMA@.restore(uuid), @SCHEMA@.purge(uuid, boolean), @SCHEMA@.trim_history(uuid[], text[], timestamptz),
1167
1245
  @SCHEMA@.rekey(uuid, jsonb, jsonb), @SCHEMA@.rekey_class(text, jsonb), @SCHEMA@.account_purge(uuid),
1168
- @SCHEMA@.reset(text, text, uuid, int), @SCHEMA@.maintain(timestamptz, int), @SCHEMA@.verify(boolean),
1246
+ @SCHEMA@.reset(text, text, uuid, int), @SCHEMA@.maintain(timestamptz, int, boolean), @SCHEMA@.verify(boolean),
1169
1247
  @SCHEMA@.session_expires(), @SCHEMA@.watch_read(xid8, bigint, timestamptz, text[], int, pg_snapshot, boolean), @SCHEMA@.watch_leaving(xid8, bigint, text[], int, pg_snapshot, xid8, bigint), @SCHEMA@.watch_head(),
1170
1248
  @SCHEMA@.history_default(), @SCHEMA@.history_policy(jsonb), @SCHEMA@.history_iv(text),
1171
1249
  @SCHEMA@.signal_mode(), @SCHEMA@.signal_key(), @SCHEMA@.class_stamp() to @APP@;