Skip to content

Commit 911bd2e

Browse files
committed
ENG: Preserve pre-migration cache revisions
1 parent e8d88a3 commit 911bd2e

12 files changed

Lines changed: 97 additions & 95 deletions

File tree

ARCHITECTURE.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -178,18 +178,22 @@ The migration boundary depends on the resource operation:
178178
- **Save:** The resource manager checks the write lock first. If the stored data is
179179
older, it attempts to store the exact original JSON under
180180
`.fmu/migration-backups/` before writing the current model. This backup is
181-
best-effort: a file-system failure is logged and does not stop the save. The newly
182-
written current data is still added to the cache.
181+
best-effort: a file-system failure is logged and does not stop the save. The
182+
original JSON is also stored as a cache revision. The newly written current
183+
data is added as another cache revision.
183184
- **Cache read:** `CacheManager` uses the migration manager to return old revisions
184185
as current validated models.
185186
- **Restore:** `CacheManager` migrates the selected revision before writing it, so
186-
the restored resource uses the current schema. Project restore operations use the
187-
existing restore changelog entry.
187+
the restored resource uses the current schema. If you roll back to an older
188+
release while the pre-migration revision is still retained, restore that revision with
189+
the older release to return the resource file to the older schema. Project restore
190+
operations use the existing restore changelog entry.
188191

189192
There is no backward migration. After a current-schema resource is saved, an older
190-
`fmu-settings` release can reject it as newer than supported. Migration backups are
191-
not loaded or restored by the library. Users must copy one back manually when they
192-
need to use the data with an older release.
193+
`fmu-settings` release can reject it as newer than supported. If the pre-migration
194+
cache revision is still retained, restore it with the older release. Migration
195+
backups are not loaded or restored by the library. Users must copy one back manually
196+
when the cache revision is no longer available.
193197

194198
See the
195199
[schema migration guide](src/fmu/settings/_migrations/README.md)

src/fmu/settings/_fmu_dir.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class FMUDirectoryBase:
4747
def __init__(
4848
self: Self,
4949
base_path: str | Path,
50-
cache_revisions: int = CacheManager.MIN_REVISIONS,
50+
cache_revisions: int = 10,
5151
*,
5252
lock_timeout_seconds: int = DEFAULT_LOCK_TIMEOUT,
5353
) -> None:
@@ -56,7 +56,8 @@ def __init__(
5656
Args:
5757
base_path: The directory containing the .fmu directory or one of its parent
5858
dirs
59-
cache_revisions: Number of revisions to retain in the cache. Minimum is 5.
59+
cache_revisions: Number of revisions to retain in the cache. Default is 10.
60+
Minimum is 5.
6061
lock_timeout_seconds: Lock expiration time in seconds. Default 20 minutes.
6162
6263
Raises:
@@ -334,20 +335,18 @@ def __init__(
334335
self.config = ProjectConfigManager(self)
335336
super().__init__(
336337
base_path,
337-
CacheManager.MIN_REVISIONS,
338+
10,
338339
lock_timeout_seconds=lock_timeout_seconds,
339340
)
340341
self._changelog = ChangelogManager(self)
341342
self._mappings = MappingsManager(self)
342343
try:
343-
max_revisions = self.config.get(
344-
"cache_max_revisions", CacheManager.MIN_REVISIONS
345-
)
344+
max_revisions = self.config.get("cache_max_revisions", 10)
346345
self._cache_manager.max_revisions = max_revisions
347346
except (FileNotFoundError, ValueError) as e:
348347
logger.warning(
349348
f"Failed to load 'cache_max_revisions' from project config. "
350-
f"Using default value '{CacheManager.MIN_REVISIONS}'. Error: {e}"
349+
f"Using default value '10'. Error: {e}"
351350
)
352351

353352
def update_validation_metadata(

src/fmu/settings/_migrations/README.md

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22

33
Use this guide when a stored `.fmu` resource needs a new schema version.
44

5-
The supported resources and their migration registries are:
5+
The supported resources and their migration function registries are:
66

77
- `ProjectConfig`: `project_config/`
88
- `UserConfig`: `user_config/`
99
- `InternalMappings`: `mappings/`
1010

1111
## Decide whether to change the schema version
1212

13-
Change the schema version when existing stored data needs a conversion before it
14-
can be used by the current model.
13+
Change the schema version when existing stored data needs a migration before it can
14+
be used by the current model.
1515

1616
### Changes that need a migration
1717

@@ -40,11 +40,12 @@ old field name.
4040
Other changes that need a migration include:
4141

4242
- Removing a stored field when its value must be moved or preserved elsewhere.
43-
- Making an optional field required and calculating its value from old data.
43+
- Making an optional field required and calculating its value from stored data from
44+
an older schema.
4445
- Changing a field from one type to another, such as a string to a list of strings.
4546
- Moving flat fields into a nested model.
46-
- Changing the meaning of a value when old data must be converted to keep its
47-
original meaning.
47+
- Changing the meaning of a value when stored data from an older schema must be
48+
converted to keep its original meaning.
4849

4950
### Changes that do not need a migration
5051

@@ -67,10 +68,6 @@ Other changes that do not normally need a migration include:
6768
- Changing documentation or field descriptions.
6869
- Adding a computed property that is not stored.
6970

70-
Old stored data must still produce the correct current model when no migration is
71-
added. If the old data needs conversion or its meaning would change, add a schema
72-
version and migration function.
73-
7471
## Schema version contract
7572

7673
Each migratable model declares one positive integer schema version. The literal and
@@ -108,15 +105,15 @@ class ProjectConfig(ResettableBaseModel):
108105

109106
schema_version: Literal[2] = 2
110107
# ... unchanged fields ...
111-
max_cache_revisions: int = Field(default=5, ge=5)
108+
max_cache_revisions: int = Field(default=10, ge=5)
112109
# ... unchanged fields ...
113110

114111
@classmethod
115112
def reset(cls: type[Self]) -> Self:
116113
"""Reset the configuration to its defaults."""
117114
return cls(
118115
# ... unchanged defaults ...
119-
max_cache_revisions=5,
116+
max_cache_revisions=10,
120117
# ... unchanged defaults ...
121118
)
122119
```
@@ -165,7 +162,7 @@ PROJECT_CONFIG_MIGRATIONS: dict[int, MigrationFunction] = {
165162
}
166163
```
167164

168-
The registry key is the source version. Key `1` registers the migration from
165+
The registry key is the source schema version. Key `1` registers the migration from
169166
version 1 to version 2.
170167

171168
Keep every migration when later versions are added:
@@ -184,7 +181,7 @@ When you add a migration:
184181
- Keep `test_migration_manager.py` and the generic resource tests unchanged unless
185182
the framework behavior changes.
186183
- In `test_resource_migration.py`, update the affected resource tests that assert
187-
its version, migration registry, or previous version data.
184+
its version, migration function registry, or previous version data.
188185
- Add resource-specific tests under `tests/test_migrations/`. For example, a
189186
`ProjectConfig` migration can use `test_project_config_migration.py`.
190187

@@ -207,7 +204,7 @@ uv run mypy src tests
207204

208205
Migration is automatic during normal use:
209206

210-
1. The resource manager reads old data.
207+
1. The resource manager reads stored data from an older schema.
211208
2. The migration manager converts it in memory.
212209
3. The resource manager returns the current validated model.
213210
4. The stored file remains unchanged until a save occurs.
@@ -217,20 +214,21 @@ On the first save:
217214
1. The write lock is checked.
218215
2. If the stored data needs migration, the resource manager tries to save a copy of
219216
the original JSON under `.fmu/migration-backups/`. This backup is separate from
220-
the normal cache, is not removed automatically, and a failure to write it does not
221-
stop the save.
217+
the cache, is not removed automatically, and a failure to write it does not
218+
stop the save. The original JSON is also added to a cache revision before
219+
the migrated data is written.
222220
3. The current model is written with the new schema version.
223-
4. When automatic caching is enabled, the newly written data is added to a cache
224-
revision.
221+
4. The newly written data is also added to a cache revision.
225222

226223
Loading a resource does not create a changelog entry. A later user update or
227224
restore uses the existing changelog behavior.
228225

229-
When an old cache revision is restored, it is migrated before it is written. The
230-
resource file therefore uses the current schema after the restore. Migration backups
231-
are only saved for manual recovery, and the library does not read or restore them. To
232-
roll back, first replace the current resource file with the appropriate backup, then
233-
run the older release.
226+
When an old cache revision is restored by the current release, it is migrated before
227+
it is written. The resource file therefore uses the current schema after the restore.
228+
If you roll back to an older release while the pre-migration revision is retained,
229+
restore that revision with the older release to return the resource file to the older
230+
schema. Migration backups are not read or restored by the library, copy one back
231+
manually when the cache revision is no longer available.
234232

235233
Migrations are forward-only. After current-schema data is saved, an older
236234
`fmu-settings` release can reject it as newer than its supported schema.

src/fmu/settings/_migrations/manager.py

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Forward-only migration support for versioned resource data."""
1+
"""Forward-only migration support for versioned data."""
22

33
from __future__ import annotations
44

@@ -20,7 +20,7 @@ class MigrationError(ValueError):
2020

2121

2222
class MigrationManager(Generic[MigratableResource]):
23-
"""Migrate versioned resource data to its current schema."""
23+
"""Migrate stored data to its current schema."""
2424

2525
def __init__(
2626
self,
@@ -31,7 +31,7 @@ def __init__(
3131
3232
Args:
3333
model_class: Current Pydantic model for the resource.
34-
migrations: Migration functions keyed by their source version.
34+
migrations: Migration functions keyed by their source schema version.
3535
3636
Raises:
3737
TypeError: If the model does not declare one positive integer
@@ -42,30 +42,30 @@ def __init__(
4242
self.current_version = self._get_current_version()
4343

4444
def migrate_resource(self, data: Any) -> MigratableResource:
45-
"""Migrate decoded JSON data with predefined migration function and validate it.
45+
"""Migrate decoded data with registered migration functions and validate it.
4646
4747
Each migration function must increment the schema version by exactly one.
4848
4949
Args:
5050
data: Decoded JSON data to migrate and validate.
5151
5252
Returns:
53-
The resource data as a validated current model.
53+
The migrated data as a validated current model.
5454
5555
Raises:
5656
MigrationError: If the data is not a JSON object, a schema version is
57-
invalid or newer than supported, a required migration is missing or
58-
fails, a migration function does not increment the schema version by
59-
one, or the final data does not match the current model.
57+
invalid or newer than supported, a required migration function is
58+
missing or fails, a migration function does not increment the schema
59+
version by one, or the final data does not match the current model.
6060
"""
6161
if not isinstance(data, dict):
6262
raise MigrationError(
6363
f"{self.model_class.__name__} resource must be a JSON object"
6464
)
65-
source_version = self._get_source_version(data)
66-
migration_steps = self._get_migration_steps(source_version)
65+
source_schema_version = self._get_source_schema_version(data)
66+
migration_steps = self._get_migration_steps(source_schema_version)
6767
migrated_data = copy.deepcopy(data) if migration_steps else data.copy()
68-
migrated_data.setdefault("schema_version", source_version)
68+
migrated_data.setdefault("schema_version", source_schema_version)
6969
for version, migration_function in migration_steps:
7070
try:
7171
migrated_data = migration_function(migrated_data)
@@ -95,43 +95,44 @@ def migrate_resource(self, data: Any) -> MigratableResource:
9595
return validated_model
9696

9797
def requires_migration(self, data: dict[str, Any]) -> bool:
98-
"""Return whether the data requires a migration before a write.
98+
"""Return whether stored data needs migration before a write.
9999
100-
This method does not run migrations. It confirms that all required forward
101-
migration steps exist, then checks whether the source schema is older than
102-
the current schema.
100+
This check verifies that all required forward migration functions exist and
101+
that the stored schema version is older than the current schema version.
103102
104103
Args:
105-
data: Existing resource data that a write would replace.
104+
data: Existing stored data that a write would replace.
106105
107106
Returns:
108-
Whether the resource data requires migration.
107+
Whether the stored data requires migration.
109108
110109
Raises:
111110
MigrationError: If the schema version is invalid or newer than supported,
112-
or a required migration step is missing.
111+
or a required migration function is missing.
113112
"""
114-
source_version = self._get_source_version(data)
115-
self._get_migration_steps(source_version)
116-
return source_version < self.current_version
113+
source_schema_version = self._get_source_schema_version(data)
114+
self._get_migration_steps(source_schema_version)
115+
return source_schema_version < self.current_version
117116

118117
def _get_migration_steps(
119-
self, source_version: int
118+
self, source_schema_version: int
120119
) -> list[tuple[int, MigrationFunction]]:
121-
"""Return and validate all migration steps needed by a source version."""
122-
if source_version > self.current_version:
120+
"""Return and validate migration steps needed by a source schema version."""
121+
if source_schema_version > self.current_version:
123122
raise MigrationError(
124-
f"{self.model_class.__name__} schema version {source_version} is newer "
125-
f"than supported version {self.current_version}; downgrade migration "
123+
f"{self.model_class.__name__} schema version {source_schema_version} "
124+
"is newer than supported version "
125+
f"{self.current_version}; downgrade migration "
126126
"is not supported"
127127
)
128128

129129
steps: list[tuple[int, MigrationFunction]] = []
130-
for version in range(source_version, self.current_version):
130+
for version in range(source_schema_version, self.current_version):
131131
migration_function = self.migrations.get(version)
132132
if migration_function is None:
133133
raise MigrationError(
134-
f"Missing {self.model_class.__name__} migration from schema "
134+
f"Missing {self.model_class.__name__} migration function from "
135+
"schema "
135136
f"version {version} to {version + 1}"
136137
)
137138
steps.append((version, migration_function))
@@ -179,8 +180,8 @@ def _get_current_version(self) -> int:
179180
)
180181
return current_version
181182

182-
def _get_source_version(self, data: dict[str, Any]) -> int:
183-
"""Get the source version, using the legacy version when it is absent."""
183+
def _get_source_schema_version(self, data: dict[str, Any]) -> int:
184+
"""Return the stored schema version, using the legacy version when absent."""
184185
if "schema_version" not in data:
185186
return LEGACY_SCHEMA_VERSION
186187
return self._validate_version(data.get("schema_version"))

src/fmu/settings/_resources/cache_manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ class CacheManager:
3939
def __init__(
4040
self: Self,
4141
fmu_dir: FMUDirectoryBase,
42-
max_revisions: int = 5,
42+
max_revisions: int = 10,
4343
) -> None:
4444
"""Initialize the cache manager.
4545
4646
Args:
4747
fmu_dir: The FMUDirectory instance.
48-
max_revisions: Maximum number of revisions to retain. Default is 5.
48+
max_revisions: Maximum number of revisions to retain. Default is 10.
4949
Values below 5 are set to 5.
5050
"""
5151
self._fmu_dir = fmu_dir

src/fmu/settings/_resources/pydantic_resource_manager.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ def _store_migration_backup(self: Self) -> None:
216216

217217
source_version = data.get("schema_version", 1)
218218
self._write_migration_backup(content, source_version)
219+
if self.automatic_caching:
220+
self.fmu_dir.cache.store_revision(self.relative_path, content)
219221

220222
def _write_migration_backup(self: Self, content: str, source_version: int) -> None:
221223
"""Write a best-effort migration backup of the original content.

src/fmu/settings/models/project_config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ class ProjectConfig(ResettableBaseModel):
114114
masterdata: Masterdata | None = None
115115
model: Model | None = None
116116
access: Access | None = None
117-
cache_max_revisions: int = Field(default=5, ge=5)
117+
cache_max_revisions: int = Field(default=10, ge=5)
118118
rms: RmsProject | None = None
119119
validation: ProjectValidation = Field(default_factory=ProjectValidation)
120120

@@ -134,7 +134,7 @@ def reset(cls: type[Self]) -> Self:
134134
masterdata=None,
135135
model=None,
136136
access=None,
137-
cache_max_revisions=5,
137+
cache_max_revisions=10,
138138
rms=None,
139139
validation=ProjectValidation(),
140140
)

0 commit comments

Comments
 (0)