Skip to content

Commit 8bd4b33

Browse files
ChromeHeartsOrbax Authors
authored andcommitted
Internal
PiperOrigin-RevId: 945821631
1 parent fbe6113 commit 8bd4b33

11 files changed

Lines changed: 1241 additions & 73 deletions

File tree

checkpoint/orbax/checkpoint/experimental/tiering_service/assets.py

Lines changed: 184 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from collections.abc import Collection, Sequence
2323
import dataclasses
2424
import datetime
25+
import uuid
2526

2627
from absl import logging
2728
from orbax.checkpoint.experimental.tiering_service import db_schema
@@ -40,6 +41,10 @@ class DeletionPendingError(ValueError):
4041
"""Raised when an operation is attempted on an asset/TierPath marked for deletion."""
4142

4243

44+
class PrefetchFailedError(ValueError):
45+
"""Raised when a prefetch operation has failed."""
46+
47+
4348
@dataclasses.dataclass
4449
class CreatePrefetchJobResult:
4550
"""Result of creating a prefetch job.
@@ -105,13 +110,18 @@ def _get_location_kwargs(sb: db_schema.StorageBackend):
105110
expires_at_pb = timestamp_pb2.Timestamp()
106111
expires_at_pb.FromDatetime(tier_path.expires_at)
107112

113+
state_val = tier_path.state or db_schema.TierPathState.UNSPECIFIED
114+
proto_state_name = f"TIER_PATH_STATE_{state_val.name}"
115+
state_pb = tiering_service_pb2.TierPathState.Value(proto_state_name)
116+
108117
return tiering_service_pb2.TierPath(
109118
id=tier_path.id,
110119
path=tier_path.path,
111120
storage_backend=proto_storage_backend,
112121
ready_at=ready_at_pb,
113122
expires_at=expires_at_pb,
114123
tier_path_uuid=tier_path.tier_path_uuid,
124+
state=state_pb,
115125
)
116126

117127

@@ -271,6 +281,9 @@ async def create_or_fetch_asset(
271281
request: tiering_service_pb2.ReserveRequest,
272282
backend: db_schema.StorageBackend,
273283
config: tiering_service_pb2.ServerConfig,
284+
*,
285+
tier_path_uuid: str,
286+
storage_path: str,
274287
) -> db_schema.Asset:
275288
"""Creates a new asset or fetches an existing one on unique constraint conflict.
276289
@@ -284,6 +297,8 @@ async def create_or_fetch_asset(
284297
request: The ReserveRequest containing path, user, and tags.
285298
backend: The StorageBackend to associate the asset with.
286299
config: The ServerConfig to get the keep-alive interval.
300+
tier_path_uuid: The pre-generated UUID for the new TierPath.
301+
storage_path: The pre-generated physical storage path.
287302
288303
Returns:
289304
The created or fetched Asset object.
@@ -302,10 +317,11 @@ async def create_or_fetch_asset(
302317
)
303318
),
304319
)
305-
storage_path = storage_backend_lib.get_storage_path(backend, request.path)
320+
backend = await session.merge(backend)
306321
tier_path = db_schema.TierPath(
307322
storage_backend=backend,
308323
path=storage_path,
324+
tier_path_uuid=tier_path_uuid,
309325
)
310326
db_asset.tier_paths.append(tier_path)
311327

@@ -383,6 +399,14 @@ async def finalize_asset(
383399
Raises:
384400
ValueError: If the asset is not in ACTIVE_WRITE state.
385401
"""
402+
stmt = (
403+
select(db_schema.Asset)
404+
.where(db_schema.Asset.asset_uuid == db_asset.asset_uuid)
405+
.options(sqlalchemy.orm.joinedload(db_schema.Asset.tier_paths))
406+
)
407+
res = await session.execute(stmt)
408+
db_asset = res.scalars().first()
409+
386410
if db_asset.state != db_schema.AssetState.ASSET_STATE_ACTIVE_WRITE:
387411
raise ValueError(
388412
f"Asset {db_asset.asset_uuid} is in state {db_asset.state.name}, but"
@@ -396,13 +420,66 @@ async def finalize_asset(
396420

397421
for tier_path in db_asset.tier_paths:
398422
tier_path.ready_at = now
423+
tier_path.state = db_schema.TierPathState.READY
399424
# TODO: b/503445463 - Set expires_at when policy is supported.
400425

401426
await session.commit()
402427
await session.refresh(db_asset, attribute_names=["updated_at"])
403428
return db_asset
404429

405430

431+
async def trigger_l0_to_l1_copy(
432+
session: AsyncSession,
433+
db_asset: db_schema.Asset,
434+
) -> None:
435+
"""Triggers copying the asset from Level 0 to Level 1 storage.
436+
437+
We assume always preserve policy, so trigger L0-L1 immediately.
438+
Generically calls them L0 and L1 as they are not always Lustre and GCS.
439+
440+
Args:
441+
session: The database session.
442+
db_asset: The Asset model instance to copy.
443+
"""
444+
# Look up Level 1 storage backend to queue the copy job
445+
stmt = select(db_schema.StorageBackend).where(
446+
db_schema.StorageBackend.level == 1
447+
)
448+
res = await session.execute(stmt)
449+
level_1_backends = res.scalars().all()
450+
451+
if len(level_1_backends) > 1:
452+
raise ValueError("Multiple Level 1 backends are not supported yet.")
453+
l1_backend = level_1_backends[0] if level_1_backends else None
454+
455+
if l1_backend is not None:
456+
l1_tp_uuid = uuid.uuid4().hex
457+
l1_path = storage_backend_lib.get_storage_path(
458+
l1_backend, db_asset.path, l1_tp_uuid
459+
)
460+
new_l1_tp = db_schema.TierPath(
461+
storage_backend=l1_backend,
462+
path=l1_path,
463+
tier_path_uuid=l1_tp_uuid,
464+
state=db_schema.TierPathState.PENDING,
465+
)
466+
db_asset.tier_paths.append(new_l1_tp)
467+
468+
db_job = db_schema.AssetJob(
469+
asset_uuid=db_asset.asset_uuid,
470+
request_type=db_schema.RequestType.REQUEST_TYPE_COPY,
471+
status=db_schema.JobStatus.JOB_STATUS_QUEUED,
472+
target_tier_path=new_l1_tp,
473+
)
474+
session.add(db_job)
475+
logging.info(
476+
"Finalize: Queued copy job to Level 1 for asset %s, target path: %s",
477+
db_asset.asset_uuid,
478+
l1_path,
479+
)
480+
await session.commit()
481+
482+
406483
async def is_delete_pending(session: AsyncSession, *, asset_uuid: str) -> bool:
407484
"""Checks if there is a pending delete job for the asset."""
408485
stmt = (
@@ -453,6 +530,7 @@ async def create_prefetch_job(
453530
*,
454531
backend: db_schema.StorageBackend,
455532
storage_path: str,
533+
tier_path_uuid: str,
456534
client_keep_alive_interval: datetime.timedelta,
457535
) -> CreatePrefetchJobResult:
458536
"""Queues a prefetch job for the given asset to the target backend.
@@ -468,6 +546,7 @@ async def create_prefetch_job(
468546
db_asset: The asset to prefetch.
469547
backend: The target storage backend (level 0).
470548
storage_path: The storage path to use for the new TierPath.
549+
tier_path_uuid: The pre-generated unique identifier for the TierPath.
471550
client_keep_alive_interval: The interval to set for the initial expires_at
472551
of the TierPath.
473552
@@ -479,6 +558,14 @@ async def create_prefetch_job(
479558
DeletionPendingError: If the asset is already marked for deletion.
480559
"""
481560

561+
stmt = (
562+
select(db_schema.Asset)
563+
.where(db_schema.Asset.asset_uuid == db_asset.asset_uuid)
564+
.options(sqlalchemy.orm.joinedload(db_schema.Asset.tier_paths))
565+
)
566+
res = await session.execute(stmt)
567+
db_asset = res.scalars().first()
568+
482569
# Check if there is already a preceding delete job
483570
if await is_delete_pending(session, asset_uuid=db_asset.asset_uuid):
484571
raise DeletionPendingError(
@@ -494,9 +581,11 @@ async def create_prefetch_job(
494581
db_asset.asset_uuid,
495582
backend.id,
496583
)
584+
backend = await session.merge(backend)
497585
new_tp = db_schema.TierPath(
498586
storage_backend=backend,
499587
path=storage_path,
588+
tier_path_uuid=tier_path_uuid,
500589
expires_at=calculate_expires_at(client_keep_alive_interval),
501590
)
502591
db_asset.tier_paths.append(new_tp)
@@ -550,13 +639,17 @@ async def prefetch_keep_alive(
550639
DeletionPendingError: If the asset associated with the TierPath is marked
551640
for deletion, or if the specific TierPath instance is marked for
552641
deletion.
642+
PrefetchFailedError: If the prefetch operation on the TierPath failed.
553643
"""
554644
stmt = select(db_schema.TierPath).filter_by(tier_path_uuid=tier_path_uuid)
555645
result = await session.execute(stmt)
556646
tp = result.scalars().first()
557647
if tp is None:
558648
return None
559649

650+
if tp.state == db_schema.TierPathState.FAILED:
651+
raise PrefetchFailedError(f"Prefetch failed for TierPath {tier_path_uuid}.")
652+
560653
if await is_delete_pending(session, asset_uuid=tp.asset_uuid):
561654
raise DeletionPendingError(f"Asset {tp.asset_uuid} is marked for deletion.")
562655

@@ -645,3 +738,93 @@ async def queue_delete_asset_job(
645738
session.add(db_job) # pyrefly: ignore[missing-attribute]
646739

647740
await session.commit()
741+
742+
743+
async def begin_delete_tier_path(
744+
session: AsyncSession,
745+
tier_path: db_schema.TierPath,
746+
) -> db_schema.TierPath:
747+
"""Transitions a tier path state to DELETE_IN_PROCESS and clears read access."""
748+
tier_path.state = db_schema.TierPathState.DELETE_IN_PROCESS
749+
tier_path.ready_at = None
750+
tier_path.expires_at = None
751+
session.add(tier_path) # pyrefly: ignore[missing-attribute]
752+
return tier_path
753+
754+
755+
async def begin_delete_asset(
756+
session: AsyncSession,
757+
db_asset: db_schema.Asset,
758+
) -> db_schema.Asset:
759+
"""Transitions asset state to DELETED and all its tier paths to DELETE_IN_PROCESS."""
760+
db_asset.state = db_schema.AssetState.ASSET_STATE_DELETED
761+
db_asset.deleted_at = datetime.datetime.now(datetime.timezone.utc)
762+
session.add(db_asset) # pyrefly: ignore[missing-attribute]
763+
stmt = (
764+
select(db_schema.Asset)
765+
.where(db_schema.Asset.asset_uuid == db_asset.asset_uuid)
766+
.options(sqlalchemy.orm.joinedload(db_schema.Asset.tier_paths))
767+
)
768+
res = await session.execute(stmt)
769+
db_asset = res.scalars().first()
770+
for tp in db_asset.tier_paths:
771+
tp.state = db_schema.TierPathState.DELETE_IN_PROCESS
772+
tp.ready_at = None
773+
tp.expires_at = None
774+
session.add(tp) # pyrefly: ignore[missing-attribute]
775+
return db_asset
776+
777+
778+
async def complete_delete_asset(
779+
session: AsyncSession,
780+
db_asset: db_schema.Asset,
781+
) -> db_schema.Asset:
782+
"""Transitions asset state to DELETED and marks all tier paths as deleted.
783+
784+
Args:
785+
session: The database session.
786+
db_asset: The Asset model instance to delete.
787+
788+
Returns:
789+
The updated Asset object.
790+
"""
791+
stmt = (
792+
select(db_schema.Asset)
793+
.where(db_schema.Asset.asset_uuid == db_asset.asset_uuid)
794+
.options(sqlalchemy.orm.joinedload(db_schema.Asset.tier_paths))
795+
)
796+
res = await session.execute(stmt)
797+
db_asset = res.scalars().first()
798+
799+
now = datetime.datetime.now(datetime.timezone.utc)
800+
db_asset.state = db_schema.AssetState.ASSET_STATE_DELETED
801+
db_asset.deleted_at = now
802+
session.add(db_asset) # pyrefly: ignore[missing-attribute]
803+
804+
for tp in db_asset.tier_paths:
805+
tp.state = db_schema.TierPathState.DELETED
806+
tp.ready_at = None
807+
tp.expires_at = None
808+
session.add(tp) # pyrefly: ignore[missing-attribute]
809+
810+
return db_asset
811+
812+
813+
async def complete_delete_tier_path(
814+
session: AsyncSession,
815+
tier_path: db_schema.TierPath,
816+
) -> db_schema.TierPath:
817+
"""Transitions a tier path state to DELETED.
818+
819+
Args:
820+
session: The database session.
821+
tier_path: The TierPath model instance to update.
822+
823+
Returns:
824+
The updated TierPath object.
825+
"""
826+
tier_path.state = db_schema.TierPathState.DELETED
827+
tier_path.ready_at = None
828+
tier_path.expires_at = None
829+
session.add(tier_path) # pyrefly: ignore[missing-attribute]
830+
return tier_path

0 commit comments

Comments
 (0)