File indexing completed on 2026-11-09 10:47:36
0001 """Publish the epicprod storage component to Snapper.
0002
0003 Design: swf-epicprod docs/STORAGE.md. The storage pass
0004 (``swf_epicprod.analytics.storage``) maintains the placement state of
0005 production data on every JLab RSE in its own store and builds the
0006 bounded projection; this module registers the component and publishes
0007 each pass's projection. A pass in which no gauge and no counter moved
0008 is affirmed unchanged with the source time advanced, so quiet hours
0009 write no snap while intervals tile.
0010 """
0011
0012 import json
0013 from dataclasses import dataclass
0014
0015 from django.db import transaction
0016 from django.utils import timezone
0017
0018 from snapper_ai.services import (
0019 ComponentUpdate,
0020 publish_component,
0021 register_component,
0022 report_component_unchanged,
0023 )
0024
0025 PUBLISHER_IDENTITY = "swf-monitor:storage"
0026 ASSESSMENT_POLICY_VERSION = "swf-storage-v1"
0027 COMPONENT_NAME = "storage"
0028 SCOPE = "epicprod"
0029
0030 PROVENANCE_KEYS = ("interval", "pass")
0031
0032 STORAGE_REGISTRATION = {
0033 "title": "Production data placement on JLab RSEs",
0034 "description": (
0035 "Production output on the JLab Rucio Storage Elements from "
0036 "registration until it settles, read by the storage pass as "
0037 "production activity selects: per disk RSE the inventory by "
0038 "replica state, campaign and root, dataset placement, rules and "
0039 "locks, the copying backlog with its ages, ghosts (registered "
0040 "files with no available replica anywhere), capacity against "
0041 "the production account's limit, and cumulative counters of "
0042 "arrivals, completed transfers and ghosts appeared; per "
0043 "campaign the disk copies, catalog quality, dataset state and "
0044 "pipeline latencies; bounded exception listings. Counters are "
0045 "monotonic from the census and differenced by every consumer."
0046 ),
0047 "visibility": "public",
0048 "owning_subsystem": "SWF production catalog",
0049 "assessment_policy": ASSESSMENT_POLICY_VERSION,
0050 "quantities": {
0051 "interval": {
0052 "path": "interval",
0053 "type": "object",
0054 "required": True,
0055 "kind": "window",
0056 "description": (
0057 "The half-open interval (start, end] this publication "
0058 "covers, from the previous pass to this one."
0059 ),
0060 },
0061 "pass": {
0062 "path": "pass",
0063 "type": "object",
0064 "required": True,
0065 "kind": "provenance",
0066 "description": (
0067 "The pass that produced this publication: mode, "
0068 "campaigns covered, files and datasets checked, "
0069 "duration, and read failures."
0070 ),
0071 },
0072 "rses": {
0073 "path": "rses",
0074 "type": "object",
0075 "required": True,
0076 "kind": "bounded_map",
0077 "max_items": 16,
0078 "description": (
0079 "Per RSE: type, capacity, inventory by state, campaign "
0080 "and root, datasets, rules, backlog, ghosts, and the "
0081 "cumulative flow counters. The key 'none' collects "
0082 "registered files with no replica row."
0083 ),
0084 },
0085 "campaigns": {
0086 "path": "campaigns",
0087 "type": "object",
0088 "required": True,
0089 "kind": "bounded_map",
0090 "max_items": 8,
0091 "description": (
0092 "Per target campaign: files and bytes, replica "
0093 "protection, unattached and count-less files, archival "
0094 "backlog, dataset state, cumulative arrival and archive "
0095 "counters, and the interval's latencies."
0096 ),
0097 },
0098 "exceptions": {
0099 "path": "exceptions",
0100 "type": "object",
0101 "required": True,
0102 "kind": "fold_remainder",
0103 "description": (
0104 "Bounded listing heads, oldest first, of ghosts, stuck "
0105 "rules and stalled datasets, with the exact remainder "
0106 "as overflow counts; the full lists are served from the "
0107 "pass's store."
0108 ),
0109 },
0110 "thresholds": {
0111 "path": "thresholds",
0112 "type": "object",
0113 "required": True,
0114 "kind": "policy",
0115 "description": "The SysConfig thresholds applied.",
0116 },
0117 "assessment": {
0118 "path": "assessment",
0119 "type": "object",
0120 "required": True,
0121 "kind": "assessment",
0122 "description": (
0123 "Per-RSE and per-campaign verdicts against the "
0124 "thresholds and the overall verdict."
0125 ),
0126 },
0127 },
0128 }
0129
0130
0131 @dataclass(frozen=True)
0132 class StoragePublication:
0133 registration_update: ComponentUpdate
0134 update: ComponentUpdate
0135 quiet: bool
0136
0137
0138 def _current_data():
0139 from snapper_ai.models import CurrentComponent
0140
0141 row = (CurrentComponent.objects
0142 .filter(scope=SCOPE, name=COMPONENT_NAME)
0143 .values("data").first())
0144 return row["data"] if row and isinstance(row["data"], dict) else None
0145
0146
0147 def _recorded_state(data):
0148 """The projection without its per-pass provenance, canonical."""
0149 stripped = {k: v for k, v in data.items() if k not in PROVENANCE_KEYS}
0150 return json.dumps(stripped, sort_keys=True, separators=(",", ":"))
0151
0152
0153 def publish_storage(data, since=None) -> StoragePublication:
0154 """Register and publish one pass's projection; affirm unchanged when
0155 the recorded state equals the current component's."""
0156 observed_at = timezone.now()
0157 current = _current_data()
0158 quiet = current is not None and (
0159 _recorded_state(current) == _recorded_state(data))
0160 with transaction.atomic():
0161 registration_update = register_component(
0162 scope=SCOPE,
0163 name=COMPONENT_NAME,
0164 publisher_identity=PUBLISHER_IDENTITY,
0165 registration=STORAGE_REGISTRATION,
0166 component_schema_version=1,
0167 )
0168 if quiet:
0169 update = report_component_unchanged(
0170 scope=SCOPE,
0171 name=COMPONENT_NAME,
0172 publisher_identity=PUBLISHER_IDENTITY,
0173 assessed_at=observed_at,
0174 source_as_of=observed_at,
0175 assessment_policy_version=ASSESSMENT_POLICY_VERSION,
0176 )
0177 else:
0178 update = publish_component(
0179 scope=SCOPE,
0180 name=COMPONENT_NAME,
0181 publisher_identity=PUBLISHER_IDENTITY,
0182 data=data,
0183 assessed_at=observed_at,
0184 source_as_of=observed_at,
0185 assessment_policy_version=ASSESSMENT_POLICY_VERSION,
0186 )
0187 return StoragePublication(
0188 registration_update=registration_update, update=update, quiet=quiet)