truthound-dashboard 1.4.4__py3-none-any.whl → 1.5.1__py3-none-any.whl
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.
- truthound_dashboard/api/alerts.py +75 -86
- truthound_dashboard/api/anomaly.py +7 -13
- truthound_dashboard/api/cross_alerts.py +38 -52
- truthound_dashboard/api/drift.py +49 -59
- truthound_dashboard/api/drift_monitor.py +234 -79
- truthound_dashboard/api/enterprise_sampling.py +498 -0
- truthound_dashboard/api/history.py +57 -5
- truthound_dashboard/api/lineage.py +3 -48
- truthound_dashboard/api/maintenance.py +104 -49
- truthound_dashboard/api/mask.py +1 -2
- truthound_dashboard/api/middleware.py +2 -1
- truthound_dashboard/api/model_monitoring.py +435 -311
- truthound_dashboard/api/notifications.py +227 -191
- truthound_dashboard/api/notifications_advanced.py +21 -20
- truthound_dashboard/api/observability.py +586 -0
- truthound_dashboard/api/plugins.py +2 -433
- truthound_dashboard/api/profile.py +199 -37
- truthound_dashboard/api/quality_reporter.py +701 -0
- truthound_dashboard/api/reports.py +7 -16
- truthound_dashboard/api/router.py +66 -0
- truthound_dashboard/api/rule_suggestions.py +5 -5
- truthound_dashboard/api/scan.py +17 -19
- truthound_dashboard/api/schedules.py +85 -50
- truthound_dashboard/api/schema_evolution.py +6 -6
- truthound_dashboard/api/schema_watcher.py +667 -0
- truthound_dashboard/api/sources.py +98 -27
- truthound_dashboard/api/tiering.py +1323 -0
- truthound_dashboard/api/triggers.py +14 -11
- truthound_dashboard/api/validations.py +12 -11
- truthound_dashboard/api/versioning.py +1 -6
- truthound_dashboard/core/__init__.py +129 -3
- truthound_dashboard/core/actions/__init__.py +62 -0
- truthound_dashboard/core/actions/custom.py +426 -0
- truthound_dashboard/core/actions/notifications.py +910 -0
- truthound_dashboard/core/actions/storage.py +472 -0
- truthound_dashboard/core/actions/webhook.py +281 -0
- truthound_dashboard/core/anomaly.py +262 -67
- truthound_dashboard/core/anomaly_explainer.py +4 -3
- truthound_dashboard/core/backends/__init__.py +67 -0
- truthound_dashboard/core/backends/base.py +299 -0
- truthound_dashboard/core/backends/errors.py +191 -0
- truthound_dashboard/core/backends/factory.py +423 -0
- truthound_dashboard/core/backends/mock_backend.py +451 -0
- truthound_dashboard/core/backends/truthound_backend.py +718 -0
- truthound_dashboard/core/checkpoint/__init__.py +87 -0
- truthound_dashboard/core/checkpoint/adapters.py +814 -0
- truthound_dashboard/core/checkpoint/checkpoint.py +491 -0
- truthound_dashboard/core/checkpoint/runner.py +270 -0
- truthound_dashboard/core/connections.py +645 -23
- truthound_dashboard/core/converters/__init__.py +14 -0
- truthound_dashboard/core/converters/truthound.py +620 -0
- truthound_dashboard/core/cross_alerts.py +540 -320
- truthound_dashboard/core/datasource_factory.py +1672 -0
- truthound_dashboard/core/drift_monitor.py +216 -20
- truthound_dashboard/core/enterprise_sampling.py +1291 -0
- truthound_dashboard/core/interfaces/__init__.py +225 -0
- truthound_dashboard/core/interfaces/actions.py +652 -0
- truthound_dashboard/core/interfaces/base.py +247 -0
- truthound_dashboard/core/interfaces/checkpoint.py +676 -0
- truthound_dashboard/core/interfaces/protocols.py +664 -0
- truthound_dashboard/core/interfaces/reporters.py +650 -0
- truthound_dashboard/core/interfaces/routing.py +646 -0
- truthound_dashboard/core/interfaces/triggers.py +619 -0
- truthound_dashboard/core/lineage.py +407 -71
- truthound_dashboard/core/model_monitoring.py +431 -3
- truthound_dashboard/core/notifications/base.py +4 -0
- truthound_dashboard/core/notifications/channels.py +501 -1203
- truthound_dashboard/core/notifications/deduplication/__init__.py +81 -115
- truthound_dashboard/core/notifications/deduplication/service.py +131 -348
- truthound_dashboard/core/notifications/dispatcher.py +202 -11
- truthound_dashboard/core/notifications/escalation/__init__.py +119 -106
- truthound_dashboard/core/notifications/escalation/engine.py +168 -358
- truthound_dashboard/core/notifications/routing/__init__.py +88 -128
- truthound_dashboard/core/notifications/routing/engine.py +90 -317
- truthound_dashboard/core/notifications/stats_aggregator.py +246 -1
- truthound_dashboard/core/notifications/throttling/__init__.py +67 -50
- truthound_dashboard/core/notifications/throttling/builder.py +117 -255
- truthound_dashboard/core/notifications/truthound_adapter.py +842 -0
- truthound_dashboard/core/phase5/collaboration.py +1 -1
- truthound_dashboard/core/plugins/lifecycle/__init__.py +0 -13
- truthound_dashboard/core/quality_reporter.py +1359 -0
- truthound_dashboard/core/report_history.py +0 -6
- truthound_dashboard/core/reporters/__init__.py +175 -14
- truthound_dashboard/core/reporters/adapters.py +943 -0
- truthound_dashboard/core/reporters/base.py +0 -3
- truthound_dashboard/core/reporters/builtin/__init__.py +18 -0
- truthound_dashboard/core/reporters/builtin/csv_reporter.py +111 -0
- truthound_dashboard/core/reporters/builtin/html_reporter.py +270 -0
- truthound_dashboard/core/reporters/builtin/json_reporter.py +127 -0
- truthound_dashboard/core/reporters/compat.py +266 -0
- truthound_dashboard/core/reporters/csv_reporter.py +2 -35
- truthound_dashboard/core/reporters/factory.py +526 -0
- truthound_dashboard/core/reporters/interfaces.py +745 -0
- truthound_dashboard/core/reporters/registry.py +1 -10
- truthound_dashboard/core/scheduler.py +165 -0
- truthound_dashboard/core/schema_evolution.py +3 -3
- truthound_dashboard/core/schema_watcher.py +1528 -0
- truthound_dashboard/core/services.py +595 -76
- truthound_dashboard/core/store_manager.py +810 -0
- truthound_dashboard/core/streaming_anomaly.py +169 -4
- truthound_dashboard/core/tiering.py +1309 -0
- truthound_dashboard/core/triggers/evaluators.py +178 -8
- truthound_dashboard/core/truthound_adapter.py +2620 -197
- truthound_dashboard/core/unified_alerts.py +23 -20
- truthound_dashboard/db/__init__.py +8 -0
- truthound_dashboard/db/database.py +8 -2
- truthound_dashboard/db/models.py +944 -25
- truthound_dashboard/db/repository.py +2 -0
- truthound_dashboard/main.py +15 -0
- truthound_dashboard/schemas/__init__.py +177 -16
- truthound_dashboard/schemas/base.py +44 -23
- truthound_dashboard/schemas/collaboration.py +19 -6
- truthound_dashboard/schemas/cross_alerts.py +19 -3
- truthound_dashboard/schemas/drift.py +61 -55
- truthound_dashboard/schemas/drift_monitor.py +67 -23
- truthound_dashboard/schemas/enterprise_sampling.py +653 -0
- truthound_dashboard/schemas/lineage.py +0 -33
- truthound_dashboard/schemas/mask.py +10 -8
- truthound_dashboard/schemas/model_monitoring.py +89 -10
- truthound_dashboard/schemas/notifications_advanced.py +13 -0
- truthound_dashboard/schemas/observability.py +453 -0
- truthound_dashboard/schemas/plugins.py +0 -280
- truthound_dashboard/schemas/profile.py +154 -247
- truthound_dashboard/schemas/quality_reporter.py +403 -0
- truthound_dashboard/schemas/reports.py +2 -2
- truthound_dashboard/schemas/rule_suggestion.py +8 -1
- truthound_dashboard/schemas/scan.py +4 -24
- truthound_dashboard/schemas/schedule.py +11 -3
- truthound_dashboard/schemas/schema_watcher.py +727 -0
- truthound_dashboard/schemas/source.py +17 -2
- truthound_dashboard/schemas/tiering.py +822 -0
- truthound_dashboard/schemas/triggers.py +16 -0
- truthound_dashboard/schemas/unified_alerts.py +7 -0
- truthound_dashboard/schemas/validation.py +0 -13
- truthound_dashboard/schemas/validators/base.py +41 -21
- truthound_dashboard/schemas/validators/business_rule_validators.py +244 -0
- truthound_dashboard/schemas/validators/localization_validators.py +273 -0
- truthound_dashboard/schemas/validators/ml_feature_validators.py +308 -0
- truthound_dashboard/schemas/validators/profiling_validators.py +275 -0
- truthound_dashboard/schemas/validators/referential_validators.py +312 -0
- truthound_dashboard/schemas/validators/registry.py +93 -8
- truthound_dashboard/schemas/validators/timeseries_validators.py +389 -0
- truthound_dashboard/schemas/versioning.py +1 -6
- truthound_dashboard/static/index.html +2 -2
- truthound_dashboard-1.5.1.dist-info/METADATA +312 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/RECORD +149 -148
- truthound_dashboard/core/plugins/hooks/__init__.py +0 -63
- truthound_dashboard/core/plugins/hooks/decorators.py +0 -367
- truthound_dashboard/core/plugins/hooks/manager.py +0 -403
- truthound_dashboard/core/plugins/hooks/protocols.py +0 -265
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +0 -584
- truthound_dashboard/core/reporters/junit_reporter.py +0 -233
- truthound_dashboard/core/reporters/markdown_reporter.py +0 -207
- truthound_dashboard/core/reporters/pdf_reporter.py +0 -209
- truthound_dashboard/static/assets/_baseUniq-BcrSP13d.js +0 -1
- truthound_dashboard/static/assets/arc-DlYjKwIL.js +0 -1
- truthound_dashboard/static/assets/architectureDiagram-VXUJARFQ-Bb2drbQM.js +0 -36
- truthound_dashboard/static/assets/blockDiagram-VD42YOAC-BlsPG1CH.js +0 -122
- truthound_dashboard/static/assets/c4Diagram-YG6GDRKO-B9JdUoaC.js +0 -10
- truthound_dashboard/static/assets/channel-Q6mHF1Hd.js +0 -1
- truthound_dashboard/static/assets/chunk-4BX2VUAB-DmyoPVuJ.js +0 -1
- truthound_dashboard/static/assets/chunk-55IACEB6-Bcz6Siv8.js +0 -1
- truthound_dashboard/static/assets/chunk-B4BG7PRW-Br3G5Rum.js +0 -165
- truthound_dashboard/static/assets/chunk-DI55MBZ5-DuM9c23u.js +0 -220
- truthound_dashboard/static/assets/chunk-FMBD7UC4-DNU-5mvT.js +0 -15
- truthound_dashboard/static/assets/chunk-QN33PNHL-Im2yNcmS.js +0 -1
- truthound_dashboard/static/assets/chunk-QZHKN3VN-kZr8XFm1.js +0 -1
- truthound_dashboard/static/assets/chunk-TZMSLE5B-Q__360q_.js +0 -1
- truthound_dashboard/static/assets/classDiagram-2ON5EDUG-vtixxUyK.js +0 -1
- truthound_dashboard/static/assets/classDiagram-v2-WZHVMYZB-vtixxUyK.js +0 -1
- truthound_dashboard/static/assets/clone-BOt2LwD0.js +0 -1
- truthound_dashboard/static/assets/cose-bilkent-S5V4N54A-CBDw6iac.js +0 -1
- truthound_dashboard/static/assets/dagre-6UL2VRFP-XdKqmmY9.js +0 -4
- truthound_dashboard/static/assets/diagram-PSM6KHXK-DAZ8nx9V.js +0 -24
- truthound_dashboard/static/assets/diagram-QEK2KX5R-BRvDTbGD.js +0 -43
- truthound_dashboard/static/assets/diagram-S2PKOQOG-bQcczUkl.js +0 -24
- truthound_dashboard/static/assets/erDiagram-Q2GNP2WA-DPje7VMN.js +0 -60
- truthound_dashboard/static/assets/flowDiagram-NV44I4VS-B7BVtFVS.js +0 -162
- truthound_dashboard/static/assets/ganttDiagram-JELNMOA3-D6WKSS7U.js +0 -267
- truthound_dashboard/static/assets/gitGraphDiagram-NY62KEGX-D3vtVd3y.js +0 -65
- truthound_dashboard/static/assets/graph-BKgNKZVp.js +0 -1
- truthound_dashboard/static/assets/index-C6JSrkHo.css +0 -1
- truthound_dashboard/static/assets/index-DkU82VsU.js +0 -1800
- truthound_dashboard/static/assets/infoDiagram-WHAUD3N6-DnNCT429.js +0 -2
- truthound_dashboard/static/assets/journeyDiagram-XKPGCS4Q-DGiMozqS.js +0 -139
- truthound_dashboard/static/assets/kanban-definition-3W4ZIXB7-BV2gUgli.js +0 -89
- truthound_dashboard/static/assets/katex-Cu_Erd72.js +0 -261
- truthound_dashboard/static/assets/layout-DI2MfQ5G.js +0 -1
- truthound_dashboard/static/assets/min-DYdgXVcT.js +0 -1
- truthound_dashboard/static/assets/mindmap-definition-VGOIOE7T-C7x4ruxz.js +0 -68
- truthound_dashboard/static/assets/pieDiagram-ADFJNKIX-CAJaAB9f.js +0 -30
- truthound_dashboard/static/assets/quadrantDiagram-AYHSOK5B-DeqwDI46.js +0 -7
- truthound_dashboard/static/assets/requirementDiagram-UZGBJVZJ-e3XDpZIM.js +0 -64
- truthound_dashboard/static/assets/sankeyDiagram-TZEHDZUN-CNnAv5Ux.js +0 -10
- truthound_dashboard/static/assets/sequenceDiagram-WL72ISMW-Dsne-Of3.js +0 -145
- truthound_dashboard/static/assets/stateDiagram-FKZM4ZOC-Ee0sQXyb.js +0 -1
- truthound_dashboard/static/assets/stateDiagram-v2-4FDKWEC3-B26KqW_W.js +0 -1
- truthound_dashboard/static/assets/timeline-definition-IT6M3QCI-DZYi2yl3.js +0 -61
- truthound_dashboard/static/assets/treemap-KMMF4GRG-CY3f8In2.js +0 -128
- truthound_dashboard/static/assets/unmerged_dictionaries-Dd7xcPWG.js +0 -1
- truthound_dashboard/static/assets/xychartDiagram-PRI3JC2R-CS7fydZZ.js +0 -7
- truthound_dashboard-1.4.4.dist-info/METADATA +0 -507
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.4.4.dist-info → truthound_dashboard-1.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1309 @@
|
|
|
1
|
+
"""Storage Tiering Service.
|
|
2
|
+
|
|
3
|
+
This module provides storage tiering functionality using truthound's
|
|
4
|
+
storage tiering module (truthound.stores.tiering).
|
|
5
|
+
|
|
6
|
+
Architecture:
|
|
7
|
+
API Endpoints
|
|
8
|
+
↓
|
|
9
|
+
TieringService
|
|
10
|
+
↓
|
|
11
|
+
TieringAdapter
|
|
12
|
+
↓
|
|
13
|
+
truthound.stores.tiering
|
|
14
|
+
- TierType
|
|
15
|
+
- StorageTier
|
|
16
|
+
- TieringConfig
|
|
17
|
+
- TierPolicy classes
|
|
18
|
+
- TierMetadataStore
|
|
19
|
+
- TieringResult
|
|
20
|
+
|
|
21
|
+
Features:
|
|
22
|
+
- Storage tier management (hot, warm, cold, archive)
|
|
23
|
+
- Policy-based migration (age, access, size, scheduled, composite, custom)
|
|
24
|
+
- Migration execution with truthound backends
|
|
25
|
+
- Access tracking for intelligent tiering
|
|
26
|
+
- Background policy evaluation and execution
|
|
27
|
+
- Migration history and statistics
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import asyncio
|
|
33
|
+
import logging
|
|
34
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
from datetime import datetime, timedelta
|
|
37
|
+
from functools import partial
|
|
38
|
+
from typing import Any, Protocol, runtime_checkable
|
|
39
|
+
from uuid import uuid4
|
|
40
|
+
|
|
41
|
+
from sqlalchemy import func, select, update
|
|
42
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
43
|
+
from sqlalchemy.orm import selectinload
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
# Thread pool for blocking truthound operations
|
|
48
|
+
_executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="tiering")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _generate_id() -> str:
|
|
52
|
+
"""Generate a unique ID."""
|
|
53
|
+
return str(uuid4())
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# =============================================================================
|
|
57
|
+
# Protocols for type checking
|
|
58
|
+
# =============================================================================
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@runtime_checkable
|
|
62
|
+
class TruthoundStore(Protocol):
|
|
63
|
+
"""Protocol for truthound store backends."""
|
|
64
|
+
|
|
65
|
+
def save(self, item_id: str, data: Any) -> str: ...
|
|
66
|
+
def get(self, item_id: str) -> Any: ...
|
|
67
|
+
def delete(self, item_id: str) -> bool: ...
|
|
68
|
+
def list(self) -> list[str]: ...
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@runtime_checkable
|
|
72
|
+
class TruthoundTierPolicy(Protocol):
|
|
73
|
+
"""Protocol for truthound tier policies."""
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def from_tier(self) -> str: ...
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def to_tier(self) -> str: ...
|
|
80
|
+
|
|
81
|
+
def should_migrate(self, info: Any) -> bool: ...
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# =============================================================================
|
|
85
|
+
# Result dataclasses
|
|
86
|
+
# =============================================================================
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class MigrationItem:
|
|
91
|
+
"""Represents an item to be migrated."""
|
|
92
|
+
|
|
93
|
+
item_id: str
|
|
94
|
+
from_tier: str
|
|
95
|
+
to_tier: str
|
|
96
|
+
size_bytes: int = 0
|
|
97
|
+
policy_id: str | None = None
|
|
98
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class MigrationResult:
|
|
103
|
+
"""Result of a single migration operation."""
|
|
104
|
+
|
|
105
|
+
item_id: str
|
|
106
|
+
from_tier: str
|
|
107
|
+
to_tier: str
|
|
108
|
+
success: bool
|
|
109
|
+
size_bytes: int = 0
|
|
110
|
+
error_message: str | None = None
|
|
111
|
+
duration_ms: float = 0.0
|
|
112
|
+
started_at: datetime = field(default_factory=datetime.utcnow)
|
|
113
|
+
completed_at: datetime | None = None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class TieringExecutionResult:
|
|
118
|
+
"""Result of a tiering execution run."""
|
|
119
|
+
|
|
120
|
+
start_time: datetime
|
|
121
|
+
end_time: datetime
|
|
122
|
+
items_scanned: int = 0
|
|
123
|
+
items_migrated: int = 0
|
|
124
|
+
items_failed: int = 0
|
|
125
|
+
bytes_migrated: int = 0
|
|
126
|
+
migrations: list[MigrationResult] = field(default_factory=list)
|
|
127
|
+
errors: list[str] = field(default_factory=list)
|
|
128
|
+
dry_run: bool = False
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def duration_seconds(self) -> float:
|
|
132
|
+
"""Calculate execution duration in seconds."""
|
|
133
|
+
return (self.end_time - self.start_time).total_seconds()
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def success_rate(self) -> float:
|
|
137
|
+
"""Calculate migration success rate."""
|
|
138
|
+
total = self.items_migrated + self.items_failed
|
|
139
|
+
return self.items_migrated / total if total > 0 else 1.0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class TierInfo:
|
|
144
|
+
"""Metadata about an item's tier placement."""
|
|
145
|
+
|
|
146
|
+
item_id: str
|
|
147
|
+
tier_name: str
|
|
148
|
+
created_at: datetime
|
|
149
|
+
migrated_at: datetime | None = None
|
|
150
|
+
access_count: int = 0
|
|
151
|
+
last_accessed: datetime | None = None
|
|
152
|
+
size_bytes: int = 0
|
|
153
|
+
next_migration: datetime | None = None
|
|
154
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# =============================================================================
|
|
158
|
+
# Tiering Adapter - truthound integration layer
|
|
159
|
+
# =============================================================================
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class TieringAdapter:
|
|
163
|
+
"""Adapter for truthound storage tiering functionality.
|
|
164
|
+
|
|
165
|
+
This adapter provides a clean interface between the dashboard and
|
|
166
|
+
truthound's tiering module, handling:
|
|
167
|
+
- Store backend creation
|
|
168
|
+
- Policy object creation
|
|
169
|
+
- Migration execution
|
|
170
|
+
- Metadata tracking
|
|
171
|
+
|
|
172
|
+
The adapter gracefully handles cases where truthound is unavailable
|
|
173
|
+
or lacks certain features.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
_truthound_available: bool | None = None
|
|
177
|
+
_tiering_available: bool | None = None
|
|
178
|
+
|
|
179
|
+
def __init__(self) -> None:
|
|
180
|
+
"""Initialize adapter."""
|
|
181
|
+
self._stores: dict[str, Any] = {}
|
|
182
|
+
self._metadata_store: Any = None
|
|
183
|
+
self._check_availability()
|
|
184
|
+
|
|
185
|
+
def _check_availability(self) -> None:
|
|
186
|
+
"""Check truthound tiering availability."""
|
|
187
|
+
if TieringAdapter._truthound_available is None:
|
|
188
|
+
try:
|
|
189
|
+
import truthound
|
|
190
|
+
TieringAdapter._truthound_available = True
|
|
191
|
+
except ImportError:
|
|
192
|
+
TieringAdapter._truthound_available = False
|
|
193
|
+
logger.warning("truthound not available, using fallback implementation")
|
|
194
|
+
|
|
195
|
+
if TieringAdapter._tiering_available is None and TieringAdapter._truthound_available:
|
|
196
|
+
try:
|
|
197
|
+
from truthound.stores.tiering.base import TierType, StorageTier, TieringConfig
|
|
198
|
+
from truthound.stores.tiering.policies import AgeBasedTierPolicy
|
|
199
|
+
TieringAdapter._tiering_available = True
|
|
200
|
+
except ImportError:
|
|
201
|
+
TieringAdapter._tiering_available = False
|
|
202
|
+
logger.warning("truthound.stores.tiering not available, using fallback")
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def is_available(self) -> bool:
|
|
206
|
+
"""Check if truthound tiering is available."""
|
|
207
|
+
return TieringAdapter._tiering_available or False
|
|
208
|
+
|
|
209
|
+
def create_store(
|
|
210
|
+
self,
|
|
211
|
+
store_type: str,
|
|
212
|
+
store_config: dict[str, Any],
|
|
213
|
+
) -> Any:
|
|
214
|
+
"""Create a truthound store backend.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
store_type: Type of store (filesystem, s3, gcs, etc.)
|
|
218
|
+
store_config: Store configuration.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Store instance or fallback store.
|
|
222
|
+
"""
|
|
223
|
+
if not self.is_available:
|
|
224
|
+
return self._create_fallback_store(store_type, store_config)
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
from truthound.stores import get_store
|
|
228
|
+
return get_store(store_type, **store_config)
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logger.warning(f"Failed to create truthound store: {e}")
|
|
231
|
+
return self._create_fallback_store(store_type, store_config)
|
|
232
|
+
|
|
233
|
+
def _create_fallback_store(
|
|
234
|
+
self,
|
|
235
|
+
store_type: str,
|
|
236
|
+
store_config: dict[str, Any],
|
|
237
|
+
) -> "FallbackStore":
|
|
238
|
+
"""Create a fallback store for when truthound is unavailable."""
|
|
239
|
+
return FallbackStore(store_type, store_config)
|
|
240
|
+
|
|
241
|
+
def create_storage_tier(
|
|
242
|
+
self,
|
|
243
|
+
name: str,
|
|
244
|
+
store: Any,
|
|
245
|
+
tier_type: str,
|
|
246
|
+
priority: int = 1,
|
|
247
|
+
cost_per_gb: float | None = None,
|
|
248
|
+
retrieval_time_ms: int | None = None,
|
|
249
|
+
metadata: dict[str, Any] | None = None,
|
|
250
|
+
) -> Any:
|
|
251
|
+
"""Create a truthound StorageTier.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
name: Tier name.
|
|
255
|
+
store: Store backend instance.
|
|
256
|
+
tier_type: Tier type (hot, warm, cold, archive).
|
|
257
|
+
priority: Read order priority.
|
|
258
|
+
cost_per_gb: Cost per GB for analysis.
|
|
259
|
+
retrieval_time_ms: Expected retrieval latency.
|
|
260
|
+
metadata: Additional metadata.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
StorageTier instance or dataclass fallback.
|
|
264
|
+
"""
|
|
265
|
+
if not self.is_available:
|
|
266
|
+
return FallbackStorageTier(
|
|
267
|
+
name=name,
|
|
268
|
+
store=store,
|
|
269
|
+
tier_type=tier_type,
|
|
270
|
+
priority=priority,
|
|
271
|
+
cost_per_gb=cost_per_gb,
|
|
272
|
+
retrieval_time_ms=retrieval_time_ms,
|
|
273
|
+
metadata=metadata or {},
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
from truthound.stores.tiering.base import StorageTier, TierType as TruthoundTierType
|
|
278
|
+
|
|
279
|
+
tier_type_enum = getattr(TruthoundTierType, tier_type.upper(), TruthoundTierType.HOT)
|
|
280
|
+
|
|
281
|
+
return StorageTier(
|
|
282
|
+
name=name,
|
|
283
|
+
store=store,
|
|
284
|
+
tier_type=tier_type_enum,
|
|
285
|
+
priority=priority,
|
|
286
|
+
cost_per_gb=cost_per_gb,
|
|
287
|
+
retrieval_time_ms=retrieval_time_ms,
|
|
288
|
+
metadata=metadata or {},
|
|
289
|
+
)
|
|
290
|
+
except Exception as e:
|
|
291
|
+
logger.warning(f"Failed to create truthound StorageTier: {e}")
|
|
292
|
+
return FallbackStorageTier(
|
|
293
|
+
name=name,
|
|
294
|
+
store=store,
|
|
295
|
+
tier_type=tier_type,
|
|
296
|
+
priority=priority,
|
|
297
|
+
cost_per_gb=cost_per_gb,
|
|
298
|
+
retrieval_time_ms=retrieval_time_ms,
|
|
299
|
+
metadata=metadata or {},
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
def create_policy(
|
|
303
|
+
self,
|
|
304
|
+
policy_type: str,
|
|
305
|
+
from_tier: str,
|
|
306
|
+
to_tier: str,
|
|
307
|
+
config: dict[str, Any],
|
|
308
|
+
direction: str = "demote",
|
|
309
|
+
) -> Any:
|
|
310
|
+
"""Create a truthound tier policy.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
policy_type: Type of policy (age_based, access_based, etc.)
|
|
314
|
+
from_tier: Source tier name.
|
|
315
|
+
to_tier: Destination tier name.
|
|
316
|
+
config: Policy-specific configuration.
|
|
317
|
+
direction: Migration direction (demote/promote).
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
TierPolicy instance or fallback.
|
|
321
|
+
"""
|
|
322
|
+
if not self.is_available:
|
|
323
|
+
return self._create_fallback_policy(
|
|
324
|
+
policy_type, from_tier, to_tier, config, direction
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
return self._create_truthound_policy(
|
|
329
|
+
policy_type, from_tier, to_tier, config, direction
|
|
330
|
+
)
|
|
331
|
+
except Exception as e:
|
|
332
|
+
logger.warning(f"Failed to create truthound policy: {e}")
|
|
333
|
+
return self._create_fallback_policy(
|
|
334
|
+
policy_type, from_tier, to_tier, config, direction
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
def _create_truthound_policy(
|
|
338
|
+
self,
|
|
339
|
+
policy_type: str,
|
|
340
|
+
from_tier: str,
|
|
341
|
+
to_tier: str,
|
|
342
|
+
config: dict[str, Any],
|
|
343
|
+
direction: str,
|
|
344
|
+
) -> Any:
|
|
345
|
+
"""Create a truthound policy from type and config."""
|
|
346
|
+
from truthound.stores.tiering.base import MigrationDirection as TruthoundDirection
|
|
347
|
+
from truthound.stores.tiering import policies as th_policies
|
|
348
|
+
|
|
349
|
+
direction_enum = (
|
|
350
|
+
TruthoundDirection.PROMOTE
|
|
351
|
+
if direction == "promote"
|
|
352
|
+
else TruthoundDirection.DEMOTE
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
if policy_type == "age_based":
|
|
356
|
+
return th_policies.AgeBasedTierPolicy(
|
|
357
|
+
from_tier=from_tier,
|
|
358
|
+
to_tier=to_tier,
|
|
359
|
+
after_days=config.get("after_days", 0),
|
|
360
|
+
after_hours=config.get("after_hours", 0),
|
|
361
|
+
direction=direction_enum,
|
|
362
|
+
)
|
|
363
|
+
elif policy_type == "access_based":
|
|
364
|
+
return th_policies.AccessBasedTierPolicy(
|
|
365
|
+
from_tier=from_tier,
|
|
366
|
+
to_tier=to_tier,
|
|
367
|
+
inactive_days=config.get("inactive_days"),
|
|
368
|
+
min_access_count=config.get("min_access_count"),
|
|
369
|
+
access_window_days=config.get("access_window_days", 7),
|
|
370
|
+
direction=direction_enum,
|
|
371
|
+
)
|
|
372
|
+
elif policy_type == "size_based":
|
|
373
|
+
return th_policies.SizeBasedTierPolicy(
|
|
374
|
+
from_tier=from_tier,
|
|
375
|
+
to_tier=to_tier,
|
|
376
|
+
min_size_bytes=config.get("min_size_bytes", 0),
|
|
377
|
+
min_size_kb=config.get("min_size_kb", 0),
|
|
378
|
+
min_size_mb=config.get("min_size_mb", 0),
|
|
379
|
+
min_size_gb=config.get("min_size_gb", 0),
|
|
380
|
+
tier_max_size_bytes=config.get("tier_max_size_bytes", 0),
|
|
381
|
+
tier_max_size_gb=config.get("tier_max_size_gb", 0),
|
|
382
|
+
direction=direction_enum,
|
|
383
|
+
)
|
|
384
|
+
elif policy_type == "scheduled":
|
|
385
|
+
return th_policies.ScheduledTierPolicy(
|
|
386
|
+
from_tier=from_tier,
|
|
387
|
+
to_tier=to_tier,
|
|
388
|
+
on_days=config.get("on_days"),
|
|
389
|
+
at_hour=config.get("at_hour"),
|
|
390
|
+
min_age_days=config.get("min_age_days", 0),
|
|
391
|
+
direction=direction_enum,
|
|
392
|
+
)
|
|
393
|
+
elif policy_type == "custom":
|
|
394
|
+
# Custom policies use a predicate function
|
|
395
|
+
predicate_expr = config.get("predicate_expression", "False")
|
|
396
|
+
|
|
397
|
+
def custom_predicate(info: Any) -> bool:
|
|
398
|
+
try:
|
|
399
|
+
# Safe evaluation with limited scope
|
|
400
|
+
local_vars = {
|
|
401
|
+
"info": info,
|
|
402
|
+
"size_bytes": getattr(info, "size_bytes", 0),
|
|
403
|
+
"access_count": getattr(info, "access_count", 0),
|
|
404
|
+
"created_at": getattr(info, "created_at", None),
|
|
405
|
+
"last_accessed": getattr(info, "last_accessed", None),
|
|
406
|
+
}
|
|
407
|
+
return bool(eval(predicate_expr, {"__builtins__": {}}, local_vars))
|
|
408
|
+
except Exception:
|
|
409
|
+
return False
|
|
410
|
+
|
|
411
|
+
return th_policies.CustomTierPolicy(
|
|
412
|
+
from_tier=from_tier,
|
|
413
|
+
to_tier=to_tier,
|
|
414
|
+
predicate=custom_predicate,
|
|
415
|
+
description=config.get("description", ""),
|
|
416
|
+
)
|
|
417
|
+
else:
|
|
418
|
+
raise ValueError(f"Unknown policy type: {policy_type}")
|
|
419
|
+
|
|
420
|
+
def _create_fallback_policy(
|
|
421
|
+
self,
|
|
422
|
+
policy_type: str,
|
|
423
|
+
from_tier: str,
|
|
424
|
+
to_tier: str,
|
|
425
|
+
config: dict[str, Any],
|
|
426
|
+
direction: str,
|
|
427
|
+
) -> "FallbackPolicy":
|
|
428
|
+
"""Create a fallback policy."""
|
|
429
|
+
return FallbackPolicy(
|
|
430
|
+
policy_type=policy_type,
|
|
431
|
+
from_tier=from_tier,
|
|
432
|
+
to_tier=to_tier,
|
|
433
|
+
config=config,
|
|
434
|
+
direction=direction,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
def create_metadata_store(self) -> Any:
|
|
438
|
+
"""Create or get the tier metadata store.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
TierMetadataStore instance.
|
|
442
|
+
"""
|
|
443
|
+
if self._metadata_store is not None:
|
|
444
|
+
return self._metadata_store
|
|
445
|
+
|
|
446
|
+
if self.is_available:
|
|
447
|
+
try:
|
|
448
|
+
from truthound.stores.tiering.base import InMemoryTierMetadataStore
|
|
449
|
+
self._metadata_store = InMemoryTierMetadataStore()
|
|
450
|
+
except Exception as e:
|
|
451
|
+
logger.warning(f"Failed to create truthound metadata store: {e}")
|
|
452
|
+
self._metadata_store = FallbackMetadataStore()
|
|
453
|
+
else:
|
|
454
|
+
self._metadata_store = FallbackMetadataStore()
|
|
455
|
+
|
|
456
|
+
return self._metadata_store
|
|
457
|
+
|
|
458
|
+
async def execute_migration(
|
|
459
|
+
self,
|
|
460
|
+
item_id: str,
|
|
461
|
+
from_store: Any,
|
|
462
|
+
to_store: Any,
|
|
463
|
+
metadata_store: Any,
|
|
464
|
+
) -> MigrationResult:
|
|
465
|
+
"""Execute a single item migration.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
item_id: ID of item to migrate.
|
|
469
|
+
from_store: Source store.
|
|
470
|
+
to_store: Destination store.
|
|
471
|
+
metadata_store: Metadata store for tracking.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
MigrationResult with migration details.
|
|
475
|
+
"""
|
|
476
|
+
started_at = datetime.utcnow()
|
|
477
|
+
from_tier = getattr(from_store, "name", "unknown")
|
|
478
|
+
to_tier = getattr(to_store, "name", "unknown")
|
|
479
|
+
|
|
480
|
+
try:
|
|
481
|
+
# Run migration in thread pool to avoid blocking
|
|
482
|
+
loop = asyncio.get_event_loop()
|
|
483
|
+
result = await loop.run_in_executor(
|
|
484
|
+
_executor,
|
|
485
|
+
partial(self._sync_migrate, item_id, from_store, to_store, metadata_store),
|
|
486
|
+
)
|
|
487
|
+
completed_at = datetime.utcnow()
|
|
488
|
+
duration_ms = (completed_at - started_at).total_seconds() * 1000
|
|
489
|
+
|
|
490
|
+
return MigrationResult(
|
|
491
|
+
item_id=item_id,
|
|
492
|
+
from_tier=from_tier,
|
|
493
|
+
to_tier=to_tier,
|
|
494
|
+
success=result.get("success", False),
|
|
495
|
+
size_bytes=result.get("size_bytes", 0),
|
|
496
|
+
error_message=result.get("error"),
|
|
497
|
+
duration_ms=duration_ms,
|
|
498
|
+
started_at=started_at,
|
|
499
|
+
completed_at=completed_at,
|
|
500
|
+
)
|
|
501
|
+
except Exception as e:
|
|
502
|
+
completed_at = datetime.utcnow()
|
|
503
|
+
duration_ms = (completed_at - started_at).total_seconds() * 1000
|
|
504
|
+
return MigrationResult(
|
|
505
|
+
item_id=item_id,
|
|
506
|
+
from_tier=from_tier,
|
|
507
|
+
to_tier=to_tier,
|
|
508
|
+
success=False,
|
|
509
|
+
error_message=str(e),
|
|
510
|
+
duration_ms=duration_ms,
|
|
511
|
+
started_at=started_at,
|
|
512
|
+
completed_at=completed_at,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
def _sync_migrate(
|
|
516
|
+
self,
|
|
517
|
+
item_id: str,
|
|
518
|
+
from_store: Any,
|
|
519
|
+
to_store: Any,
|
|
520
|
+
metadata_store: Any,
|
|
521
|
+
) -> dict[str, Any]:
|
|
522
|
+
"""Synchronous migration operation (runs in thread pool)."""
|
|
523
|
+
try:
|
|
524
|
+
# Get item from source
|
|
525
|
+
data = from_store.get(item_id)
|
|
526
|
+
if data is None:
|
|
527
|
+
return {"success": False, "error": f"Item {item_id} not found in source tier"}
|
|
528
|
+
|
|
529
|
+
# Calculate size
|
|
530
|
+
size_bytes = len(str(data).encode("utf-8")) if data else 0
|
|
531
|
+
|
|
532
|
+
# Save to destination
|
|
533
|
+
to_store.save(item_id, data)
|
|
534
|
+
|
|
535
|
+
# Delete from source
|
|
536
|
+
from_store.delete(item_id)
|
|
537
|
+
|
|
538
|
+
# Update metadata
|
|
539
|
+
if metadata_store:
|
|
540
|
+
info = metadata_store.get_info(item_id)
|
|
541
|
+
if info:
|
|
542
|
+
info.tier_name = getattr(to_store, "name", "unknown")
|
|
543
|
+
info.migrated_at = datetime.utcnow()
|
|
544
|
+
metadata_store.save_info(info)
|
|
545
|
+
|
|
546
|
+
return {"success": True, "size_bytes": size_bytes}
|
|
547
|
+
except Exception as e:
|
|
548
|
+
return {"success": False, "error": str(e)}
|
|
549
|
+
|
|
550
|
+
def evaluate_policy(
|
|
551
|
+
self,
|
|
552
|
+
policy: Any,
|
|
553
|
+
tier_info: TierInfo,
|
|
554
|
+
) -> bool:
|
|
555
|
+
"""Evaluate if an item should be migrated by a policy.
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
policy: Tier policy to evaluate.
|
|
559
|
+
tier_info: Item's tier information.
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
True if item should be migrated.
|
|
563
|
+
"""
|
|
564
|
+
try:
|
|
565
|
+
if hasattr(policy, "should_migrate"):
|
|
566
|
+
return policy.should_migrate(tier_info)
|
|
567
|
+
elif isinstance(policy, FallbackPolicy):
|
|
568
|
+
return policy.evaluate(tier_info)
|
|
569
|
+
return False
|
|
570
|
+
except Exception as e:
|
|
571
|
+
logger.warning(f"Policy evaluation failed: {e}")
|
|
572
|
+
return False
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
# =============================================================================
|
|
576
|
+
# Fallback implementations
|
|
577
|
+
# =============================================================================
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
@dataclass
|
|
581
|
+
class FallbackStore:
|
|
582
|
+
"""Fallback store implementation when truthound is unavailable."""
|
|
583
|
+
|
|
584
|
+
store_type: str
|
|
585
|
+
config: dict[str, Any]
|
|
586
|
+
_data: dict[str, Any] = field(default_factory=dict)
|
|
587
|
+
|
|
588
|
+
def save(self, item_id: str, data: Any) -> str:
|
|
589
|
+
self._data[item_id] = data
|
|
590
|
+
return item_id
|
|
591
|
+
|
|
592
|
+
def get(self, item_id: str) -> Any:
|
|
593
|
+
return self._data.get(item_id)
|
|
594
|
+
|
|
595
|
+
def delete(self, item_id: str) -> bool:
|
|
596
|
+
if item_id in self._data:
|
|
597
|
+
del self._data[item_id]
|
|
598
|
+
return True
|
|
599
|
+
return False
|
|
600
|
+
|
|
601
|
+
def list(self) -> list[str]:
|
|
602
|
+
return list(self._data.keys())
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
@dataclass
|
|
606
|
+
class FallbackStorageTier:
|
|
607
|
+
"""Fallback StorageTier when truthound is unavailable."""
|
|
608
|
+
|
|
609
|
+
name: str
|
|
610
|
+
store: Any
|
|
611
|
+
tier_type: str
|
|
612
|
+
priority: int = 1
|
|
613
|
+
cost_per_gb: float | None = None
|
|
614
|
+
retrieval_time_ms: int | None = None
|
|
615
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
@dataclass
|
|
619
|
+
class FallbackPolicy:
|
|
620
|
+
"""Fallback policy implementation."""
|
|
621
|
+
|
|
622
|
+
policy_type: str
|
|
623
|
+
from_tier: str
|
|
624
|
+
to_tier: str
|
|
625
|
+
config: dict[str, Any]
|
|
626
|
+
direction: str = "demote"
|
|
627
|
+
|
|
628
|
+
def evaluate(self, info: TierInfo) -> bool:
|
|
629
|
+
"""Evaluate if item should be migrated."""
|
|
630
|
+
now = datetime.utcnow()
|
|
631
|
+
|
|
632
|
+
if self.policy_type == "age_based":
|
|
633
|
+
after_days = self.config.get("after_days", 0)
|
|
634
|
+
after_hours = self.config.get("after_hours", 0)
|
|
635
|
+
threshold = timedelta(days=after_days, hours=after_hours)
|
|
636
|
+
age = now - info.created_at
|
|
637
|
+
return age >= threshold
|
|
638
|
+
|
|
639
|
+
elif self.policy_type == "access_based":
|
|
640
|
+
inactive_days = self.config.get("inactive_days")
|
|
641
|
+
min_access_count = self.config.get("min_access_count")
|
|
642
|
+
|
|
643
|
+
if inactive_days and info.last_accessed:
|
|
644
|
+
inactive_time = now - info.last_accessed
|
|
645
|
+
if inactive_time >= timedelta(days=inactive_days):
|
|
646
|
+
return True
|
|
647
|
+
|
|
648
|
+
if min_access_count:
|
|
649
|
+
return info.access_count >= min_access_count
|
|
650
|
+
|
|
651
|
+
return False
|
|
652
|
+
|
|
653
|
+
elif self.policy_type == "size_based":
|
|
654
|
+
total_min_bytes = (
|
|
655
|
+
self.config.get("min_size_bytes", 0)
|
|
656
|
+
+ self.config.get("min_size_kb", 0) * 1024
|
|
657
|
+
+ self.config.get("min_size_mb", 0) * 1024 * 1024
|
|
658
|
+
+ self.config.get("min_size_gb", 0) * 1024 * 1024 * 1024
|
|
659
|
+
)
|
|
660
|
+
return info.size_bytes >= total_min_bytes
|
|
661
|
+
|
|
662
|
+
elif self.policy_type == "scheduled":
|
|
663
|
+
on_days = self.config.get("on_days")
|
|
664
|
+
at_hour = self.config.get("at_hour")
|
|
665
|
+
min_age_days = self.config.get("min_age_days", 0)
|
|
666
|
+
|
|
667
|
+
if on_days and now.weekday() not in on_days:
|
|
668
|
+
return False
|
|
669
|
+
if at_hour is not None and now.hour != at_hour:
|
|
670
|
+
return False
|
|
671
|
+
if min_age_days:
|
|
672
|
+
age = now - info.created_at
|
|
673
|
+
if age < timedelta(days=min_age_days):
|
|
674
|
+
return False
|
|
675
|
+
return True
|
|
676
|
+
|
|
677
|
+
elif self.policy_type == "custom":
|
|
678
|
+
predicate_expr = self.config.get("predicate_expression", "False")
|
|
679
|
+
try:
|
|
680
|
+
local_vars = {
|
|
681
|
+
"info": info,
|
|
682
|
+
"size_bytes": info.size_bytes,
|
|
683
|
+
"access_count": info.access_count,
|
|
684
|
+
"created_at": info.created_at,
|
|
685
|
+
"last_accessed": info.last_accessed,
|
|
686
|
+
}
|
|
687
|
+
return bool(eval(predicate_expr, {"__builtins__": {}}, local_vars))
|
|
688
|
+
except Exception:
|
|
689
|
+
return False
|
|
690
|
+
|
|
691
|
+
return False
|
|
692
|
+
|
|
693
|
+
def should_migrate(self, info: Any) -> bool:
|
|
694
|
+
"""Alias for evaluate for protocol compatibility."""
|
|
695
|
+
if isinstance(info, TierInfo):
|
|
696
|
+
return self.evaluate(info)
|
|
697
|
+
# Convert to TierInfo if needed
|
|
698
|
+
tier_info = TierInfo(
|
|
699
|
+
item_id=getattr(info, "item_id", ""),
|
|
700
|
+
tier_name=getattr(info, "tier_name", ""),
|
|
701
|
+
created_at=getattr(info, "created_at", datetime.utcnow()),
|
|
702
|
+
migrated_at=getattr(info, "migrated_at", None),
|
|
703
|
+
access_count=getattr(info, "access_count", 0),
|
|
704
|
+
last_accessed=getattr(info, "last_accessed", None),
|
|
705
|
+
size_bytes=getattr(info, "size_bytes", 0),
|
|
706
|
+
)
|
|
707
|
+
return self.evaluate(tier_info)
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
class FallbackMetadataStore:
|
|
711
|
+
"""Fallback metadata store when truthound is unavailable."""
|
|
712
|
+
|
|
713
|
+
def __init__(self) -> None:
|
|
714
|
+
self._data: dict[str, TierInfo] = {}
|
|
715
|
+
|
|
716
|
+
def save_info(self, info: TierInfo) -> None:
|
|
717
|
+
self._data[info.item_id] = info
|
|
718
|
+
|
|
719
|
+
def get_info(self, item_id: str) -> TierInfo | None:
|
|
720
|
+
return self._data.get(item_id)
|
|
721
|
+
|
|
722
|
+
def delete_info(self, item_id: str) -> bool:
|
|
723
|
+
if item_id in self._data:
|
|
724
|
+
del self._data[item_id]
|
|
725
|
+
return True
|
|
726
|
+
return False
|
|
727
|
+
|
|
728
|
+
def list_by_tier(self, tier_name: str) -> list[TierInfo]:
|
|
729
|
+
return [info for info in self._data.values() if info.tier_name == tier_name]
|
|
730
|
+
|
|
731
|
+
def update_access(self, item_id: str) -> None:
|
|
732
|
+
if item_id in self._data:
|
|
733
|
+
self._data[item_id].access_count += 1
|
|
734
|
+
self._data[item_id].last_accessed = datetime.utcnow()
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
# =============================================================================
|
|
738
|
+
# Singleton adapter instance
|
|
739
|
+
# =============================================================================
|
|
740
|
+
|
|
741
|
+
_adapter_instance: TieringAdapter | None = None
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def get_tiering_adapter() -> TieringAdapter:
|
|
745
|
+
"""Get the singleton tiering adapter instance."""
|
|
746
|
+
global _adapter_instance
|
|
747
|
+
if _adapter_instance is None:
|
|
748
|
+
_adapter_instance = TieringAdapter()
|
|
749
|
+
return _adapter_instance
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
# =============================================================================
|
|
753
|
+
# Tiering Service
|
|
754
|
+
# =============================================================================
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
class TieringService:
|
|
758
|
+
"""Service for storage tiering operations.
|
|
759
|
+
|
|
760
|
+
This service provides comprehensive storage tiering using truthound's
|
|
761
|
+
tiering module. It manages:
|
|
762
|
+
- Tier configuration and lifecycle
|
|
763
|
+
- Policy evaluation and execution
|
|
764
|
+
- Migration operations
|
|
765
|
+
- Statistics and reporting
|
|
766
|
+
- Background processing
|
|
767
|
+
|
|
768
|
+
The service uses TieringAdapter to interact with truthound,
|
|
769
|
+
maintaining loose coupling with the underlying library.
|
|
770
|
+
"""
|
|
771
|
+
|
|
772
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
773
|
+
"""Initialize service.
|
|
774
|
+
|
|
775
|
+
Args:
|
|
776
|
+
session: Async database session.
|
|
777
|
+
"""
|
|
778
|
+
self._session = session
|
|
779
|
+
self._adapter = get_tiering_adapter()
|
|
780
|
+
self._tier_stores: dict[str, Any] = {}
|
|
781
|
+
self._tier_objects: dict[str, Any] = {}
|
|
782
|
+
self._policies: dict[str, Any] = {}
|
|
783
|
+
|
|
784
|
+
# =========================================================================
|
|
785
|
+
# Tier Management
|
|
786
|
+
# =========================================================================
|
|
787
|
+
|
|
788
|
+
async def initialize_tier(self, tier_id: str) -> Any:
|
|
789
|
+
"""Initialize a tier's store backend.
|
|
790
|
+
|
|
791
|
+
Args:
|
|
792
|
+
tier_id: Tier ID from database.
|
|
793
|
+
|
|
794
|
+
Returns:
|
|
795
|
+
Initialized StorageTier object.
|
|
796
|
+
"""
|
|
797
|
+
from truthound_dashboard.db.models import StorageTierModel
|
|
798
|
+
|
|
799
|
+
# Check cache
|
|
800
|
+
if tier_id in self._tier_objects:
|
|
801
|
+
return self._tier_objects[tier_id]
|
|
802
|
+
|
|
803
|
+
# Load from database
|
|
804
|
+
tier = await self._session.get(StorageTierModel, tier_id)
|
|
805
|
+
if not tier:
|
|
806
|
+
raise ValueError(f"Tier '{tier_id}' not found")
|
|
807
|
+
|
|
808
|
+
# Create store backend
|
|
809
|
+
store = self._adapter.create_store(tier.store_type, tier.store_config)
|
|
810
|
+
self._tier_stores[tier_id] = store
|
|
811
|
+
|
|
812
|
+
# Create StorageTier object
|
|
813
|
+
tier_obj = self._adapter.create_storage_tier(
|
|
814
|
+
name=tier.name,
|
|
815
|
+
store=store,
|
|
816
|
+
tier_type=tier.tier_type,
|
|
817
|
+
priority=tier.priority,
|
|
818
|
+
cost_per_gb=tier.cost_per_gb,
|
|
819
|
+
retrieval_time_ms=tier.retrieval_time_ms,
|
|
820
|
+
metadata=tier.metadata,
|
|
821
|
+
)
|
|
822
|
+
self._tier_objects[tier_id] = tier_obj
|
|
823
|
+
|
|
824
|
+
return tier_obj
|
|
825
|
+
|
|
826
|
+
async def get_tier_store(self, tier_id: str) -> Any:
|
|
827
|
+
"""Get the store backend for a tier.
|
|
828
|
+
|
|
829
|
+
Args:
|
|
830
|
+
tier_id: Tier ID.
|
|
831
|
+
|
|
832
|
+
Returns:
|
|
833
|
+
Store backend instance.
|
|
834
|
+
"""
|
|
835
|
+
if tier_id not in self._tier_stores:
|
|
836
|
+
await self.initialize_tier(tier_id)
|
|
837
|
+
return self._tier_stores.get(tier_id)
|
|
838
|
+
|
|
839
|
+
# =========================================================================
|
|
840
|
+
# Policy Management
|
|
841
|
+
# =========================================================================
|
|
842
|
+
|
|
843
|
+
async def initialize_policy(self, policy_id: str) -> Any:
|
|
844
|
+
"""Initialize a policy object.
|
|
845
|
+
|
|
846
|
+
Args:
|
|
847
|
+
policy_id: Policy ID from database.
|
|
848
|
+
|
|
849
|
+
Returns:
|
|
850
|
+
Initialized policy object.
|
|
851
|
+
"""
|
|
852
|
+
from truthound_dashboard.db.models import TierPolicyModel
|
|
853
|
+
|
|
854
|
+
# Check cache
|
|
855
|
+
if policy_id in self._policies:
|
|
856
|
+
return self._policies[policy_id]
|
|
857
|
+
|
|
858
|
+
# Load from database
|
|
859
|
+
result = await self._session.execute(
|
|
860
|
+
select(TierPolicyModel)
|
|
861
|
+
.options(
|
|
862
|
+
selectinload(TierPolicyModel.from_tier),
|
|
863
|
+
selectinload(TierPolicyModel.to_tier),
|
|
864
|
+
selectinload(TierPolicyModel.children),
|
|
865
|
+
)
|
|
866
|
+
.where(TierPolicyModel.id == policy_id)
|
|
867
|
+
)
|
|
868
|
+
policy = result.scalar_one_or_none()
|
|
869
|
+
if not policy:
|
|
870
|
+
raise ValueError(f"Policy '{policy_id}' not found")
|
|
871
|
+
|
|
872
|
+
# Handle composite policies
|
|
873
|
+
if policy.policy_type == "composite":
|
|
874
|
+
return await self._initialize_composite_policy(policy)
|
|
875
|
+
|
|
876
|
+
# Create policy object
|
|
877
|
+
policy_obj = self._adapter.create_policy(
|
|
878
|
+
policy_type=policy.policy_type,
|
|
879
|
+
from_tier=policy.from_tier.name if policy.from_tier else "",
|
|
880
|
+
to_tier=policy.to_tier.name if policy.to_tier else "",
|
|
881
|
+
config=policy.config,
|
|
882
|
+
direction=policy.direction,
|
|
883
|
+
)
|
|
884
|
+
self._policies[policy_id] = policy_obj
|
|
885
|
+
|
|
886
|
+
return policy_obj
|
|
887
|
+
|
|
888
|
+
async def _initialize_composite_policy(self, policy: Any) -> Any:
|
|
889
|
+
"""Initialize a composite policy with children.
|
|
890
|
+
|
|
891
|
+
Args:
|
|
892
|
+
policy: Composite policy model.
|
|
893
|
+
|
|
894
|
+
Returns:
|
|
895
|
+
Composite policy object.
|
|
896
|
+
"""
|
|
897
|
+
if not self._adapter.is_available:
|
|
898
|
+
# Create fallback composite
|
|
899
|
+
child_policies = []
|
|
900
|
+
for child in policy.children:
|
|
901
|
+
child_obj = self._adapter.create_policy(
|
|
902
|
+
policy_type=child.policy_type,
|
|
903
|
+
from_tier=child.from_tier.name if child.from_tier else "",
|
|
904
|
+
to_tier=child.to_tier.name if child.to_tier else "",
|
|
905
|
+
config=child.config,
|
|
906
|
+
direction=child.direction,
|
|
907
|
+
)
|
|
908
|
+
child_policies.append(child_obj)
|
|
909
|
+
|
|
910
|
+
return FallbackCompositePolicy(
|
|
911
|
+
from_tier=policy.from_tier.name if policy.from_tier else "",
|
|
912
|
+
to_tier=policy.to_tier.name if policy.to_tier else "",
|
|
913
|
+
policies=child_policies,
|
|
914
|
+
require_all=policy.config.get("require_all", True),
|
|
915
|
+
direction=policy.direction,
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
try:
|
|
919
|
+
from truthound.stores.tiering.policies import CompositeTierPolicy
|
|
920
|
+
from truthound.stores.tiering.base import MigrationDirection as TruthoundDirection
|
|
921
|
+
|
|
922
|
+
direction_enum = (
|
|
923
|
+
TruthoundDirection.PROMOTE
|
|
924
|
+
if policy.direction == "promote"
|
|
925
|
+
else TruthoundDirection.DEMOTE
|
|
926
|
+
)
|
|
927
|
+
|
|
928
|
+
# Initialize child policies
|
|
929
|
+
child_policies = []
|
|
930
|
+
for child in policy.children:
|
|
931
|
+
child_obj = await self.initialize_policy(child.id)
|
|
932
|
+
child_policies.append(child_obj)
|
|
933
|
+
|
|
934
|
+
return CompositeTierPolicy(
|
|
935
|
+
from_tier=policy.from_tier.name,
|
|
936
|
+
to_tier=policy.to_tier.name,
|
|
937
|
+
policies=child_policies,
|
|
938
|
+
require_all=policy.config.get("require_all", True),
|
|
939
|
+
direction=direction_enum,
|
|
940
|
+
)
|
|
941
|
+
except Exception as e:
|
|
942
|
+
logger.warning(f"Failed to create composite policy: {e}")
|
|
943
|
+
# Fallback
|
|
944
|
+
child_policies = []
|
|
945
|
+
for child in policy.children:
|
|
946
|
+
child_obj = self._adapter.create_policy(
|
|
947
|
+
policy_type=child.policy_type,
|
|
948
|
+
from_tier=child.from_tier.name if child.from_tier else "",
|
|
949
|
+
to_tier=child.to_tier.name if child.to_tier else "",
|
|
950
|
+
config=child.config,
|
|
951
|
+
direction=child.direction,
|
|
952
|
+
)
|
|
953
|
+
child_policies.append(child_obj)
|
|
954
|
+
|
|
955
|
+
return FallbackCompositePolicy(
|
|
956
|
+
from_tier=policy.from_tier.name if policy.from_tier else "",
|
|
957
|
+
to_tier=policy.to_tier.name if policy.to_tier else "",
|
|
958
|
+
policies=child_policies,
|
|
959
|
+
require_all=policy.config.get("require_all", True),
|
|
960
|
+
direction=policy.direction,
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
# =========================================================================
|
|
964
|
+
# Migration Execution
|
|
965
|
+
# =========================================================================
|
|
966
|
+
|
|
967
|
+
async def execute_policy(
|
|
968
|
+
self,
|
|
969
|
+
policy_id: str,
|
|
970
|
+
dry_run: bool = False,
|
|
971
|
+
batch_size: int = 100,
|
|
972
|
+
) -> TieringExecutionResult:
|
|
973
|
+
"""Execute a tier policy.
|
|
974
|
+
|
|
975
|
+
Args:
|
|
976
|
+
policy_id: Policy ID to execute.
|
|
977
|
+
dry_run: If True, don't actually migrate, just report what would happen.
|
|
978
|
+
batch_size: Maximum items to process.
|
|
979
|
+
|
|
980
|
+
Returns:
|
|
981
|
+
Execution result with migration details.
|
|
982
|
+
"""
|
|
983
|
+
from truthound_dashboard.db.models import TierPolicyModel, TierMigrationHistoryModel
|
|
984
|
+
|
|
985
|
+
start_time = datetime.utcnow()
|
|
986
|
+
result = TieringExecutionResult(
|
|
987
|
+
start_time=start_time,
|
|
988
|
+
end_time=start_time,
|
|
989
|
+
dry_run=dry_run,
|
|
990
|
+
)
|
|
991
|
+
|
|
992
|
+
try:
|
|
993
|
+
# Load policy
|
|
994
|
+
db_policy = await self._session.execute(
|
|
995
|
+
select(TierPolicyModel)
|
|
996
|
+
.options(
|
|
997
|
+
selectinload(TierPolicyModel.from_tier),
|
|
998
|
+
selectinload(TierPolicyModel.to_tier),
|
|
999
|
+
)
|
|
1000
|
+
.where(TierPolicyModel.id == policy_id)
|
|
1001
|
+
)
|
|
1002
|
+
policy_model = db_policy.scalar_one_or_none()
|
|
1003
|
+
if not policy_model:
|
|
1004
|
+
result.errors.append(f"Policy '{policy_id}' not found")
|
|
1005
|
+
result.end_time = datetime.utcnow()
|
|
1006
|
+
return result
|
|
1007
|
+
|
|
1008
|
+
if not policy_model.is_active:
|
|
1009
|
+
result.errors.append(f"Policy '{policy_model.name}' is not active")
|
|
1010
|
+
result.end_time = datetime.utcnow()
|
|
1011
|
+
return result
|
|
1012
|
+
|
|
1013
|
+
# Initialize tiers
|
|
1014
|
+
from_tier = await self.initialize_tier(policy_model.from_tier_id)
|
|
1015
|
+
to_tier = await self.initialize_tier(policy_model.to_tier_id)
|
|
1016
|
+
from_store = self._tier_stores[policy_model.from_tier_id]
|
|
1017
|
+
to_store = self._tier_stores[policy_model.to_tier_id]
|
|
1018
|
+
|
|
1019
|
+
# Initialize policy
|
|
1020
|
+
policy_obj = await self.initialize_policy(policy_id)
|
|
1021
|
+
|
|
1022
|
+
# Get metadata store
|
|
1023
|
+
metadata_store = self._adapter.create_metadata_store()
|
|
1024
|
+
|
|
1025
|
+
# Get items in source tier
|
|
1026
|
+
items_to_check = self._get_tier_items(from_store, metadata_store, batch_size)
|
|
1027
|
+
result.items_scanned = len(items_to_check)
|
|
1028
|
+
|
|
1029
|
+
# Evaluate and migrate
|
|
1030
|
+
for item_info in items_to_check:
|
|
1031
|
+
should_migrate = self._adapter.evaluate_policy(policy_obj, item_info)
|
|
1032
|
+
|
|
1033
|
+
if should_migrate:
|
|
1034
|
+
if dry_run:
|
|
1035
|
+
result.items_migrated += 1
|
|
1036
|
+
result.bytes_migrated += item_info.size_bytes
|
|
1037
|
+
result.migrations.append(
|
|
1038
|
+
MigrationResult(
|
|
1039
|
+
item_id=item_info.item_id,
|
|
1040
|
+
from_tier=policy_model.from_tier.name,
|
|
1041
|
+
to_tier=policy_model.to_tier.name,
|
|
1042
|
+
success=True,
|
|
1043
|
+
size_bytes=item_info.size_bytes,
|
|
1044
|
+
)
|
|
1045
|
+
)
|
|
1046
|
+
else:
|
|
1047
|
+
# Execute actual migration
|
|
1048
|
+
migration_result = await self._adapter.execute_migration(
|
|
1049
|
+
item_id=item_info.item_id,
|
|
1050
|
+
from_store=from_store,
|
|
1051
|
+
to_store=to_store,
|
|
1052
|
+
metadata_store=metadata_store,
|
|
1053
|
+
)
|
|
1054
|
+
result.migrations.append(migration_result)
|
|
1055
|
+
|
|
1056
|
+
if migration_result.success:
|
|
1057
|
+
result.items_migrated += 1
|
|
1058
|
+
result.bytes_migrated += migration_result.size_bytes
|
|
1059
|
+
|
|
1060
|
+
# Record in history
|
|
1061
|
+
history_entry = TierMigrationHistoryModel(
|
|
1062
|
+
id=_generate_id(),
|
|
1063
|
+
policy_id=policy_id,
|
|
1064
|
+
item_id=item_info.item_id,
|
|
1065
|
+
from_tier_id=policy_model.from_tier_id,
|
|
1066
|
+
to_tier_id=policy_model.to_tier_id,
|
|
1067
|
+
size_bytes=migration_result.size_bytes,
|
|
1068
|
+
started_at=migration_result.started_at,
|
|
1069
|
+
completed_at=migration_result.completed_at,
|
|
1070
|
+
status="completed",
|
|
1071
|
+
)
|
|
1072
|
+
self._session.add(history_entry)
|
|
1073
|
+
else:
|
|
1074
|
+
result.items_failed += 1
|
|
1075
|
+
result.errors.append(
|
|
1076
|
+
f"Migration failed for {item_info.item_id}: {migration_result.error_message}"
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
# Record failure
|
|
1080
|
+
history_entry = TierMigrationHistoryModel(
|
|
1081
|
+
id=_generate_id(),
|
|
1082
|
+
policy_id=policy_id,
|
|
1083
|
+
item_id=item_info.item_id,
|
|
1084
|
+
from_tier_id=policy_model.from_tier_id,
|
|
1085
|
+
to_tier_id=policy_model.to_tier_id,
|
|
1086
|
+
size_bytes=0,
|
|
1087
|
+
started_at=migration_result.started_at,
|
|
1088
|
+
completed_at=migration_result.completed_at,
|
|
1089
|
+
status="failed",
|
|
1090
|
+
error_message=migration_result.error_message,
|
|
1091
|
+
)
|
|
1092
|
+
self._session.add(history_entry)
|
|
1093
|
+
|
|
1094
|
+
if not dry_run:
|
|
1095
|
+
await self._session.commit()
|
|
1096
|
+
|
|
1097
|
+
except Exception as e:
|
|
1098
|
+
logger.exception(f"Policy execution failed: {e}")
|
|
1099
|
+
result.errors.append(str(e))
|
|
1100
|
+
|
|
1101
|
+
result.end_time = datetime.utcnow()
|
|
1102
|
+
return result
|
|
1103
|
+
|
|
1104
|
+
def _get_tier_items(
|
|
1105
|
+
self,
|
|
1106
|
+
store: Any,
|
|
1107
|
+
metadata_store: Any,
|
|
1108
|
+
limit: int,
|
|
1109
|
+
) -> list[TierInfo]:
|
|
1110
|
+
"""Get items in a tier with their metadata.
|
|
1111
|
+
|
|
1112
|
+
Args:
|
|
1113
|
+
store: Store backend.
|
|
1114
|
+
metadata_store: Metadata store.
|
|
1115
|
+
limit: Maximum items to return.
|
|
1116
|
+
|
|
1117
|
+
Returns:
|
|
1118
|
+
List of TierInfo objects.
|
|
1119
|
+
"""
|
|
1120
|
+
items = []
|
|
1121
|
+
tier_name = getattr(store, "name", "unknown")
|
|
1122
|
+
|
|
1123
|
+
try:
|
|
1124
|
+
item_ids = store.list()[:limit] if hasattr(store, "list") else []
|
|
1125
|
+
|
|
1126
|
+
for item_id in item_ids:
|
|
1127
|
+
info = metadata_store.get_info(item_id)
|
|
1128
|
+
if info:
|
|
1129
|
+
items.append(info)
|
|
1130
|
+
else:
|
|
1131
|
+
# Create basic info if not tracked
|
|
1132
|
+
items.append(
|
|
1133
|
+
TierInfo(
|
|
1134
|
+
item_id=item_id,
|
|
1135
|
+
tier_name=tier_name,
|
|
1136
|
+
created_at=datetime.utcnow(),
|
|
1137
|
+
)
|
|
1138
|
+
)
|
|
1139
|
+
except Exception as e:
|
|
1140
|
+
logger.warning(f"Failed to list tier items: {e}")
|
|
1141
|
+
|
|
1142
|
+
return items
|
|
1143
|
+
|
|
1144
|
+
async def migrate_item(
|
|
1145
|
+
self,
|
|
1146
|
+
item_id: str,
|
|
1147
|
+
from_tier_id: str,
|
|
1148
|
+
to_tier_id: str,
|
|
1149
|
+
) -> MigrationResult:
|
|
1150
|
+
"""Migrate a single item between tiers.
|
|
1151
|
+
|
|
1152
|
+
Args:
|
|
1153
|
+
item_id: Item to migrate.
|
|
1154
|
+
from_tier_id: Source tier ID.
|
|
1155
|
+
to_tier_id: Destination tier ID.
|
|
1156
|
+
|
|
1157
|
+
Returns:
|
|
1158
|
+
Migration result.
|
|
1159
|
+
"""
|
|
1160
|
+
from truthound_dashboard.db.models import TierMigrationHistoryModel
|
|
1161
|
+
|
|
1162
|
+
from_store = await self.get_tier_store(from_tier_id)
|
|
1163
|
+
to_store = await self.get_tier_store(to_tier_id)
|
|
1164
|
+
metadata_store = self._adapter.create_metadata_store()
|
|
1165
|
+
|
|
1166
|
+
result = await self._adapter.execute_migration(
|
|
1167
|
+
item_id=item_id,
|
|
1168
|
+
from_store=from_store,
|
|
1169
|
+
to_store=to_store,
|
|
1170
|
+
metadata_store=metadata_store,
|
|
1171
|
+
)
|
|
1172
|
+
|
|
1173
|
+
# Record in history
|
|
1174
|
+
history_entry = TierMigrationHistoryModel(
|
|
1175
|
+
id=_generate_id(),
|
|
1176
|
+
item_id=item_id,
|
|
1177
|
+
from_tier_id=from_tier_id,
|
|
1178
|
+
to_tier_id=to_tier_id,
|
|
1179
|
+
size_bytes=result.size_bytes,
|
|
1180
|
+
started_at=result.started_at,
|
|
1181
|
+
completed_at=result.completed_at,
|
|
1182
|
+
status="completed" if result.success else "failed",
|
|
1183
|
+
error_message=result.error_message,
|
|
1184
|
+
)
|
|
1185
|
+
self._session.add(history_entry)
|
|
1186
|
+
await self._session.commit()
|
|
1187
|
+
|
|
1188
|
+
return result
|
|
1189
|
+
|
|
1190
|
+
# =========================================================================
|
|
1191
|
+
# Access Tracking
|
|
1192
|
+
# =========================================================================
|
|
1193
|
+
|
|
1194
|
+
async def record_access(self, item_id: str, tier_id: str) -> None:
|
|
1195
|
+
"""Record an access to an item for intelligent tiering.
|
|
1196
|
+
|
|
1197
|
+
Args:
|
|
1198
|
+
item_id: Accessed item ID.
|
|
1199
|
+
tier_id: Current tier ID.
|
|
1200
|
+
"""
|
|
1201
|
+
metadata_store = self._adapter.create_metadata_store()
|
|
1202
|
+
|
|
1203
|
+
info = metadata_store.get_info(item_id)
|
|
1204
|
+
if info:
|
|
1205
|
+
metadata_store.update_access(item_id)
|
|
1206
|
+
else:
|
|
1207
|
+
# Create new tracking entry
|
|
1208
|
+
info = TierInfo(
|
|
1209
|
+
item_id=item_id,
|
|
1210
|
+
tier_name=tier_id,
|
|
1211
|
+
created_at=datetime.utcnow(),
|
|
1212
|
+
access_count=1,
|
|
1213
|
+
last_accessed=datetime.utcnow(),
|
|
1214
|
+
)
|
|
1215
|
+
metadata_store.save_info(info)
|
|
1216
|
+
|
|
1217
|
+
# =========================================================================
|
|
1218
|
+
# Background Processing
|
|
1219
|
+
# =========================================================================
|
|
1220
|
+
|
|
1221
|
+
async def process_due_policies(self) -> list[TieringExecutionResult]:
|
|
1222
|
+
"""Process all active policies that are due for execution.
|
|
1223
|
+
|
|
1224
|
+
Returns:
|
|
1225
|
+
List of execution results.
|
|
1226
|
+
"""
|
|
1227
|
+
from truthound_dashboard.db.models import TierPolicyModel, TieringConfigModel
|
|
1228
|
+
|
|
1229
|
+
results = []
|
|
1230
|
+
|
|
1231
|
+
# Get active config
|
|
1232
|
+
config_result = await self._session.execute(
|
|
1233
|
+
select(TieringConfigModel).where(TieringConfigModel.is_active == True)
|
|
1234
|
+
)
|
|
1235
|
+
config = config_result.scalar_one_or_none()
|
|
1236
|
+
|
|
1237
|
+
batch_size = config.batch_size if config else 100
|
|
1238
|
+
|
|
1239
|
+
# Get active policies
|
|
1240
|
+
policies_result = await self._session.execute(
|
|
1241
|
+
select(TierPolicyModel)
|
|
1242
|
+
.where(TierPolicyModel.is_active == True)
|
|
1243
|
+
.where(TierPolicyModel.parent_id == None) # Only root policies
|
|
1244
|
+
.order_by(TierPolicyModel.priority)
|
|
1245
|
+
)
|
|
1246
|
+
policies = policies_result.scalars().all()
|
|
1247
|
+
|
|
1248
|
+
for policy in policies:
|
|
1249
|
+
try:
|
|
1250
|
+
result = await self.execute_policy(
|
|
1251
|
+
policy_id=policy.id,
|
|
1252
|
+
dry_run=False,
|
|
1253
|
+
batch_size=batch_size,
|
|
1254
|
+
)
|
|
1255
|
+
results.append(result)
|
|
1256
|
+
except Exception as e:
|
|
1257
|
+
logger.exception(f"Failed to execute policy {policy.id}: {e}")
|
|
1258
|
+
results.append(
|
|
1259
|
+
TieringExecutionResult(
|
|
1260
|
+
start_time=datetime.utcnow(),
|
|
1261
|
+
end_time=datetime.utcnow(),
|
|
1262
|
+
errors=[str(e)],
|
|
1263
|
+
)
|
|
1264
|
+
)
|
|
1265
|
+
|
|
1266
|
+
return results
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
@dataclass
|
|
1270
|
+
class FallbackCompositePolicy:
|
|
1271
|
+
"""Fallback composite policy implementation."""
|
|
1272
|
+
|
|
1273
|
+
from_tier: str
|
|
1274
|
+
to_tier: str
|
|
1275
|
+
policies: list[Any]
|
|
1276
|
+
require_all: bool = True
|
|
1277
|
+
direction: str = "demote"
|
|
1278
|
+
|
|
1279
|
+
def should_migrate(self, info: Any) -> bool:
|
|
1280
|
+
"""Evaluate composite policy."""
|
|
1281
|
+
results = [p.should_migrate(info) for p in self.policies]
|
|
1282
|
+
|
|
1283
|
+
if self.require_all:
|
|
1284
|
+
return all(results)
|
|
1285
|
+
return any(results)
|
|
1286
|
+
|
|
1287
|
+
def evaluate(self, info: TierInfo) -> bool:
|
|
1288
|
+
"""Alias for should_migrate."""
|
|
1289
|
+
return self.should_migrate(info)
|
|
1290
|
+
|
|
1291
|
+
|
|
1292
|
+
# =============================================================================
|
|
1293
|
+
# Scheduler Integration
|
|
1294
|
+
# =============================================================================
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
async def process_tiering_policies(session: AsyncSession) -> list[TieringExecutionResult]:
|
|
1298
|
+
"""Background task to process tiering policies.
|
|
1299
|
+
|
|
1300
|
+
This function is called by the scheduler to process all due policies.
|
|
1301
|
+
|
|
1302
|
+
Args:
|
|
1303
|
+
session: Database session.
|
|
1304
|
+
|
|
1305
|
+
Returns:
|
|
1306
|
+
List of execution results.
|
|
1307
|
+
"""
|
|
1308
|
+
service = TieringService(session)
|
|
1309
|
+
return await service.process_due_policies()
|