aipmodel 0.2.65__tar.gz → 0.2.67__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aipmodel
3
- Version: 0.2.65
3
+ Version: 0.2.67
4
4
  Summary: SDK for model registration, versioning, and storage
5
5
  Home-page: https://github.com/AIP-MLOPS/model-registry
6
6
  Author: AIP MLOPS Team
@@ -51,6 +51,12 @@ S3_MAX_CONCURRENCY="10"
51
51
  S3_UPLOAD_WORKERS="4"
52
52
  S3_DOWNLOAD_WORKERS="4"
53
53
 
54
+ # Auto-retry for transient connection drops during large transfers
55
+ S3_MAX_RETRIES="5" # per-file retries before giving up
56
+ S3_RETRY_BACKOFF_SECONDS="2" # base backoff, grows exponentially per attempt
57
+ S3_RETRY_MAX_DELAY_SECONDS="30" # cap on a single backoff sleep
58
+ S3_BOTO_MAX_ATTEMPTS="10" # botocore request-level retry attempts (adaptive)
59
+
54
60
  ```
55
61
 
56
62
  > **Note:** `CLEARML_WEB_HOST` is new. If not provided, the SDK will default to the API host, which might result in incorrect clickable links in the output.
@@ -68,6 +74,50 @@ S3_DOWNLOAD_WORKERS="4"
68
74
 
69
75
  Nothing is ever logged to a file or hidden — all output goes to stdout via `print()`, so it's visible wherever the process runs.
70
76
 
77
+ ## Resilient Transfers (Auto-Retry)
78
+
79
+ Large multi-GB uploads/downloads over Ceph/RGW occasionally hit a **transient**
80
+ connection drop mid-stream (`IncompleteRead` / `ProtocolError` / connection
81
+ reset). The SDK now **retries these automatically**, so `add_model`,
82
+ `get_model` and `download` complete without manual intervention:
83
+
84
+ - Each **individual file** transfer is retried on a transient error with
85
+ exponential backoff (`S3_RETRY_BACKOFF_SECONDS`, capped by
86
+ `S3_RETRY_MAX_DELAY_SECONDS`), up to `S3_MAX_RETRIES` times. Downloads reset
87
+ the partial file before each retry so no corrupt bytes remain.
88
+ - The underlying boto3 client also uses botocore's **adaptive** request-level
89
+ retries (`S3_BOTO_MAX_ATTEMPTS`).
90
+ - **Non-transient** errors (bad input, auth, not-found) are **not** retried —
91
+ they fail immediately as before.
92
+ - Fully backward compatible: method signatures, return values and the final
93
+ `ValueError` raised on ultimate failure are unchanged. All knobs are
94
+ `.env`-overridable with sane defaults.
95
+
96
+ ## Concurrent Adds
97
+
98
+ `add_model` is **concurrency-safe** — you can run many adds at the same time:
99
+
100
+ - Adds with **different model names** run **fully in parallel** (own ClearML id,
101
+ own S3 prefix — no interference).
102
+ - Adds with the **same `(project, model_name)`** are **serialized** by a
103
+ process-wide lock. Otherwise two simultaneous same-name adds would race on the
104
+ existence check + auto version-number computation and could create a
105
+ **duplicate model** or **overwrite a version**. With the lock the second add
106
+ waits for the first, then correctly continues to the next version
107
+ (`_v2` → `_v3`, …) — no duplicates, no lost versions.
108
+ - Uploads are **staged**: files go to a temp S3 path first and are only moved to
109
+ the final version path once the whole upload succeeds; any failure rolls back
110
+ (deletes the new model if it was new + the temp folder) and re-raises, so a
111
+ crashed/aborted add never leaves a half-written version.
112
+ - **Duplicate `(model_name, version)` is rejected**: re-adding an existing
113
+ explicit version raises `ValueError` instead of silently overwriting it (omit
114
+ `version` to auto-generate the next unique one).
115
+ - **Orphaned temp folders** left behind by a hard-killed upload are swept
116
+ automatically on the next add to that model.
117
+ - The lock is **per-process**. One server process (e.g. a single `uvicorn`
118
+ worker) is fully protected; for multiple workers/replicas add a distributed
119
+ lock (ClearML/Redis).
120
+
71
121
  ## Access Control (ACL)
72
122
 
73
123
  ACL is **off by default** and fully optional. Toggle it with one env var:
@@ -6,5 +6,5 @@ check_latest_version("aipmodel")
6
6
 
7
7
  __all__ = ["MLOpsManager", "CephS3Manager"]
8
8
 
9
- __version__ = "0.2.65"
9
+ __version__ = "0.2.67"
10
10
  __description__ = "SDK for model registration, versioning, and storage"
@@ -10,6 +10,7 @@ from huggingface_hub import snapshot_download
10
10
  from typing import Optional, List, Dict, Any
11
11
  import json
12
12
  import logging
13
+ import threading
13
14
  import time
14
15
  import re
15
16
  from tqdm import tqdm
@@ -22,6 +23,25 @@ load_dotenv()
22
23
 
23
24
  logger = logging.getLogger(__name__)
24
25
 
26
+ # Process-wide locks that serialize concurrent add_model calls targeting the SAME
27
+ # (project, model_name). Two simultaneous adds of the same name would otherwise
28
+ # race on the existence check + version-number computation and could create a
29
+ # duplicate model or clobber a version. Different names lock on different keys and
30
+ # still run fully in parallel. NOTE: per-process — with multiple API workers /
31
+ # replicas a distributed lock would be required for the same guarantee.
32
+ _ADD_MODEL_LOCKS = {}
33
+ _ADD_MODEL_LOCKS_GUARD = threading.Lock()
34
+
35
+
36
+ def _get_add_model_lock(project_id, model_name):
37
+ key = (project_id, model_name)
38
+ with _ADD_MODEL_LOCKS_GUARD:
39
+ lock = _ADD_MODEL_LOCKS.get(key)
40
+ if lock is None:
41
+ lock = threading.Lock()
42
+ _ADD_MODEL_LOCKS[key] = lock
43
+ return lock
44
+
25
45
  class ProjectsAPI:
26
46
  def __init__(self, post, verbose):
27
47
  self.verbose = verbose
@@ -667,9 +687,59 @@ class MLOpsManager:
667
687
  if os.path.exists(tmp_dir):
668
688
  shutil.rmtree(tmp_dir)
669
689
 
690
+ def _cleanup_orphan_temp_folders(self, manager, model_id, keep_prefix=None):
691
+ """
692
+ Best-effort removal of leftover `*_tmp_*` staging folders under a model —
693
+ orphans from a previously interrupted/crashed upload (where rollback
694
+ never ran). Safe because add_model holds a per-(project, model_name)
695
+ lock, so no other add for this model is running concurrently. Never
696
+ raises: cleanup must never break an otherwise-valid add.
697
+ """
698
+ try:
699
+ base = f"_models/{model_id}/"
700
+ keys = manager.list_folder_contents(base)
701
+ temp_prefixes = set()
702
+ for key in keys:
703
+ rel = key[len(base):] if key.startswith(base) else key
704
+ top = rel.split("/", 1)[0]
705
+ prefix = f"{base}{top}/"
706
+ if "_tmp_" in top and prefix != keep_prefix:
707
+ temp_prefixes.add(prefix)
708
+ for prefix in temp_prefixes:
709
+ try:
710
+ manager.delete_folder(prefix)
711
+ if self.verbose:
712
+ print(f"[SDK_INFO] Cleaned orphan temp folder: {prefix}")
713
+ except Exception:
714
+ pass
715
+ except Exception:
716
+ pass
717
+
670
718
  def add_model(self, source_type, model_name=None, version=None, source_path=None, code_path=None, category=None, tags=None,
671
719
  public=False, external_ceph_endpoint_url=None, external_ceph_bucket_name=None, external_ceph_access_key=None, external_ceph_secret_key=None,
672
720
  model_folder_name=None):
721
+ """
722
+ Register/add a model version. Concurrency-safe: adds that target the SAME
723
+ (project, model_name) are serialized by a process-wide lock so simultaneous
724
+ same-name adds cannot create a duplicate model or collide on the auto
725
+ version number; adds with different names run fully in parallel. All logic
726
+ lives in _add_model_impl — signature, behavior and return value are
727
+ unchanged (this wrapper only adds the lock).
728
+ """
729
+ with _get_add_model_lock(self.project_id, model_name):
730
+ return self._add_model_impl(
731
+ source_type, model_name=model_name, version=version, source_path=source_path,
732
+ code_path=code_path, category=category, tags=tags, public=public,
733
+ external_ceph_endpoint_url=external_ceph_endpoint_url,
734
+ external_ceph_bucket_name=external_ceph_bucket_name,
735
+ external_ceph_access_key=external_ceph_access_key,
736
+ external_ceph_secret_key=external_ceph_secret_key,
737
+ model_folder_name=model_folder_name,
738
+ )
739
+
740
+ def _add_model_impl(self, source_type, model_name=None, version=None, source_path=None, code_path=None, category=None, tags=None,
741
+ public=False, external_ceph_endpoint_url=None, external_ceph_bucket_name=None, external_ceph_access_key=None, external_ceph_secret_key=None,
742
+ model_folder_name=None):
673
743
 
674
744
  if self.verbose:
675
745
  print(f"[SDK_INFO] Adding model: {model_name} (Source: {source_type}, Public: {public})")
@@ -775,6 +845,17 @@ class MLOpsManager:
775
845
  max_version_number = vn
776
846
  except Exception:
777
847
  pass
848
+
849
+ # Reject a duplicate (model_name, version): if the caller passed an
850
+ # explicit version that already exists, fail instead of silently
851
+ # overwriting it. Omitting version (auto-generation) is always unique.
852
+ if version and version in versions_map:
853
+ error_message = (
854
+ f"Version '{version}' already exists for model '{model_name}'. "
855
+ f"Choose a different version name, or omit 'version' to auto-generate the next one."
856
+ )
857
+ print(f"[SDK_FAIL] {error_message}")
858
+ raise ValueError(error_message)
778
859
  else:
779
860
  if self.verbose:
780
861
  print("[SDK_INFO] Creating new model entry...")
@@ -803,6 +884,11 @@ class MLOpsManager:
803
884
  else:
804
885
  model_folder_name = "unknown"
805
886
 
887
+ # Sweep orphaned temp folders from any previously interrupted upload
888
+ # (safe under the per-name lock). New models have none.
889
+ if not is_new_model:
890
+ self._cleanup_orphan_temp_folders(current_manager, model_id)
891
+
806
892
  dest_version_path = f"_models/{model_id}/{version}/"
807
893
  temp_suffix = self.generate_random_string()
808
894
  dest_temp_path = f"_models/{model_id}/{version}_tmp_{temp_suffix}/"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aipmodel
3
- Version: 0.2.65
3
+ Version: 0.2.67
4
4
  Summary: SDK for model registration, versioning, and storage
5
5
  Home-page: https://github.com/AIP-MLOPS/model-registry
6
6
  Author: AIP MLOPS Team
@@ -31,7 +31,7 @@ version, description = read_meta()
31
31
 
32
32
  setup(
33
33
  name="aipmodel",
34
- version="0.2.65",
34
+ version="0.2.67",
35
35
  description=description,
36
36
  author="AIP MLOPS Team",
37
37
  author_email="mohmmadweb@gmail.com",
File without changes
File without changes
File without changes