Back to home page

EIC code displayed by LXR

 
 

    


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 # Keys that change on every pass without a change of recorded state.
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)