Skip to content

Support graceful script isolation fallback when filesystem does not support locking#1838

Open
rhysparry wants to merge 56 commits intomainfrom
rhys/eft-383/graceful-lock-fallback
Open

Support graceful script isolation fallback when filesystem does not support locking#1838
rhysparry wants to merge 56 commits intomainfrom
rhys/eft-383/graceful-lock-fallback

Conversation

@rhysparry
Copy link
Copy Markdown
Contributor

@rhysparry rhysparry commented Mar 19, 2026

Script isolation lock directory fallback for filesystems without full locking support

Motivation

Calamari's script isolation mechanism uses filesystem file locks to serialise or permit concurrent script execution. The lock file has historically been written into the Tentacle working directory (TentacleHome).

On some deployment targets the working directory resides on a filesystem that does not fully support file locking semantics — NFS mounts, certain FUSE-based filesystems, and some SMB shares being common examples. On these filesystems, lock acquisition either silently succeeds when it should block, or throws errors that are not meaningful to the caller. The result is that script isolation is silently broken: scripts that should be mutually exclusive may run concurrently, or scripts that should run in parallel are incorrectly serialised (or errored).

This PR introduces runtime filesystem lock capability detection and an automatic fallback to a temporary directory that does support locking, so that script isolation remains as strong as the host OS allows — without requiring any manual configuration from operators.


What changed

New abstractions

File Purpose
ScriptIsolation/LockCapability.cs Enum: Unsupported, ExclusiveOnly, Supported; unknown capability represented as null (LockCapability?)
ScriptIsolation/LockFile.cs Sealed record pairing a LockDirectory with a specific FileInfo; exposes Open, Supports, Exists, and Delete, and owns the nested LockHandle implementation
ScriptIsolation/LockDirectory.cs Sealed record capturing a DirectoryInfo + its detected LockCapability; exposes GetLockFile and Supports(LockType)
ScriptIsolation/LockDirectoryFactory.cs + ILockDirectoryFactory.cs Injectable factory that implements the fallback algorithm (Create) and the live 4-probe DetectLockSupport logic; injected with IMountedDrivesProvider, IFileLockService, and ITemporaryDirectoryFallbackProvider
ScriptIsolation/CachedDriveInfo.cs Snapshot of a drive/mount's format, type, and detected lock capability; LockSupport returns a statically-known LockCapability? based on format/drive-type heuristics, or null when the drive is unrecognised (triggering live detection)
ScriptIsolation/MountedDrives.cs + ICachedDriveInfoProvider.cs Maps a path to its best-matching drive via longest-prefix matching with symlink resolution; implements ICachedDriveInfoProvider
ScriptIsolation/SystemMountedDrivesProvider.cs + IMountedDrivesProvider.cs Injectable provider that calls DriveInfo.GetDrives() and constructs a MountedDrives instance — separates OS enumeration from path-matching logic
ScriptIsolation/TemporaryDirectoryFallbackProvider.cs + ITemporaryDirectoryFallbackProvider.cs Platform-aware enumeration of candidate temp directories to fall back to
ScriptIsolation/RequestedLockOptions.cs + RequestedLockOptionsFactory.cs Record capturing the validated, parsed lock request (type, mutex name, timeout, preferred directory); factory creates it from CommonOptions.ScriptIsolationOptions
ScriptIsolation/LockOptionsFactory.cs Creates the final LockOptions by resolving the lock directory via ILockDirectoryFactory and applying the degradation policy (UseExclusiveIfSharedIsNotSupported) — always promotes a shared lock to exclusive when shared locking is unavailable
ScriptIsolation/ScriptIsolationEnforcer.cs + IScriptIsolationEnforcer.cs Injectable class (replacing the static Isolation class) that orchestrates lock acquisition via RequestedLockOptionsFactory and LockOptionsFactory
ScriptIsolation/ScriptIsolationModule.cs Autofac Module that registers all script-isolation services (ScriptIsolationEnforcer, LockDirectoryFactory, RequestedLockOptionsFactory, LockOptionsFactory, providers, etc.)
ScriptIsolation/LockAcquisitionResiliencePipelineBuilder.cs Builds the Polly retry/timeout pipeline; extracted from LockOptions to keep the record type focused
ScriptIsolation/IFileLockService.cs + FileLockService.cs Injectable abstraction over CreateDirectory and AcquireLock — enables hermetic unit tests
ScriptIsolation/IPathResolutionService.cs + DefaultPathResolutionService.cs + PathResolutionServiceExtensions.cs Injectable abstraction and extension over Path.GetFullPath and symlink resolution — necessary to correctly match paths on macOS where /tmp → /private/tmp

Modified files

File Change
ScriptIsolation/LockOptions.cs Simplified to a pure data record; IsFullySupported/IsSupported renamed to BothSharedAndExclusiveAreSupported/RequestedLockTypeIsSupported; FromScriptIsolationOptionsOrNull and BuildLockAcquisitionPipeline removed (moved to dedicated factories/builder)
ScriptIsolation/FileLock.cs Minor: adapts to LockFile record shape
Plumbing/Commands/CommonOptions.cs scriptIsolationSharedFallback CLI option and PromoteToExclusiveLockWhenSharedLockUnavailable property removed — lock promotion is now unconditional

Removed files

File Reason
ScriptIsolation/Isolation.cs Replaced by ScriptIsolationEnforcer (injectable) + IScriptIsolationEnforcer

Decision trees

Phase 1 — Lock directory selection (LockDirectoryFactory.Create)

flowchart TD
    Start([Candidate path]) --> MapDrive[Map candidate to its drive]
    MapDrive --> CheckHeuristic{drive?.LockSupport?}
    CheckHeuristic -- Supported --> ReturnCandidate([Return candidate\nLockCapability.Supported])
    CheckHeuristic -- null no drive or unknown format --> ProbeCandidateDrive
    CheckHeuristic -- ExclusiveOnly or Unsupported --> IterateFallbacks

    ProbeCandidateDrive[DetectLockSupport on candidate path]
    ProbeCandidateDrive --> CandidateProbeResult{Candidate\ndetected capability?}
    CandidateProbeResult -- Supported --> ReturnCandidate2([Return candidate\nLockCapability.Supported])
    CandidateProbeResult -- ExclusiveOnly or Unsupported or null --> IterateFallbacks

    IterateFallbacks[Iterate temp fallback directories]
    IterateFallbacks --> HasTemp{More temp\ncandidates?}
    HasTemp -- No --> AnyExclusiveTemp
    HasTemp -- Yes --> MapTempDrive[Map temp dir to its drive]
    MapTempDrive --> CheckTempHeuristic{tempDrive?.LockSupport?}
    CheckTempHeuristic -- Supported --> ReturnTemp([Return temp dir\nLockCapability.Supported])
    CheckTempHeuristic -- null no drive or unknown format --> ProbeTemp[DetectLockSupport on temp path]
    CheckTempHeuristic -- ExclusiveOnly --> RecordFirstExclusive[Record as best-so-far\nif none recorded]
    ProbeTemp --> TempResult{Temp capability?}
    TempResult -- Supported --> ReturnTemp
    TempResult -- ExclusiveOnly --> RecordFirstExclusive
    TempResult -- Unsupported or null --> HasTemp
    RecordFirstExclusive --> HasTemp

    AnyExclusiveTemp{Any ExclusiveOnly\ntemp found?}
    AnyExclusiveTemp -- Yes, and candidate is ExclusiveOnly --> ReturnCandidate3([Return candidate\nLockCapability.ExclusiveOnly])
    AnyExclusiveTemp -- Yes, and candidate is Unsupported/null --> ReturnBestTemp([Return best temp dir\nLockCapability.ExclusiveOnly])
    AnyExclusiveTemp -- No --> ReturnUnsupported([Return candidate\nLockCapability.Unsupported])
Loading

Key design choice: CachedDriveInfo.LockSupport acts as a fast heuristic — known-good filesystem formats (ntfs, ext4, apfs, tmpfs, etc.) return Supported immediately without probing. When LockSupport returns null (unknown format, network drive with no override, or no matching drive at all), LockDirectoryFactory.DetectLockSupport is called to empirically verify lock semantics. When both the preferred directory and the best temp are ExclusiveOnly, the preferred directory is still returned — using a temp directory offers no additional isolation benefit and introduces unnecessary path indirection.


Phase 2 — Lock options resolution (LockOptionsFactory.UseExclusiveIfSharedIsNotSupported)

flowchart TD
    Start([LockOptions\nType + LockDirectory capability]) --> FullySupported{BothSharedAndExclusiveAreSupported?\nShared + Exclusive work\ncorrectly}

    FullySupported -- Yes --> ReturnOriginal([Use requested lock\nNo warning])
    FullySupported -- No --> IsSupported{RequestedLockTypeIsSupported?\nRequested LockType\nis supported}

    IsSupported -- Yes, Exclusive requested --> ReturnExclusive([Use Exclusive lock\nNo warning])

    IsSupported -- No, Shared requested\non ExclusiveOnly dir --> ExclusiveAvail{Supports\nExclusive?}
    ExclusiveAvail -- Yes --> PromoteToExclusive([Promote to Exclusive lock\nWarn: shared unavailable])

    ExclusiveAvail -- No --> NoLockUnsupported([No lock acquired\nWarn: no isolation available])
Loading

Shared locks are always promoted to exclusive when shared locking is unavailable. The scriptIsolationSharedFallback CLI option and the PromoteToExclusiveLockWhenSharedLockUnavailable flag have been removed — there is no longer a user-configurable "skip" behaviour.


Areas especially relevant for review

LockDirectoryFactory.DetectLockSupport — live filesystem probing

Four sequential probe operations are run against a temporary .tmp file in the lock directory to empirically verify lock semantics. The probes use TimeSpan.Zero timeout so they fail fast if the filesystem rejects the operation. The test file is always cleaned up in a finally block. Any exception during probing is treated conservatively as Unsupported. Reviewers should check whether the probe-file cleanup is sufficient and whether the TimeSpan.Zero strategy is reliable across all target filesystems.

PathResolutionServiceExtensions.ResolvePath — ancestor-walk symlink resolution

The ancestor-walk is necessary because a lock directory might not exist yet at the time of drive matching (e.g. first ever run). The algorithm peels off non-existent path segments from the end, resolves the nearest existing ancestor's symlink, then re-attaches the original tail. This is especially important on macOS where Path.GetFullPath("/tmp/x") returns /tmp/x but the drive is mounted at /private/tmp. All five GetFullPath exception types are caught and cause the input to be returned unchanged.

MountedDrives.GetAssociatedDrive — longest-prefix mount matching

The match is done on the symlink-resolved path, with platform-appropriate case sensitivity (OrdinalIgnoreCase on Windows/macOS, Ordinal on Linux). Reviewers should verify the StartsWith prefix check correctly handles the case where the drive root IS the path (no trailing separator needed since RootDirectory.FullName ends with a separator on all .NET platforms).

TemporaryDirectoryFallbackProvider.GetFallbackCandidates — platform-specific candidate order

On Linux, /dev/shm is included as a candidate after /tmp. tmpfs-backed /dev/shm is one of the most reliably lock-capable paths on systems where the working directory is NFS-mounted. The candidates are namespace-scoped using the last segment of the preferred directory path to prevent cross-deployment lock file collisions.

LockOptionsFactory.UseExclusiveIfSharedIsNotSupported — unconditional promotion policy

When a shared lock is requested but only exclusive locking is supported, the lock is always promoted to exclusive and a warning is logged. There is no longer a user-configurable flag to skip this promotion. If no locking at all is supported, null is returned and a warning is logged.


Test coverage

LockDirectoryFixture — 1,084 lines, all groups tagged RunOnceOnWindowsAndLinux

All tests in this fixture use injected fakes (FakeLockService, FakePathResolutionService, FakeMountedDrivesProvider, FakeTemporaryDirectoryFallbackProvider) and run entirely in-memory — no real filesystem access.

Group What is tested
ALockDirectory.Supports All LockCapability × LockType combinations (5 cases)
BCachedDriveInfo.LockSupport static Known-good formats (apfs, ntfs, ext4, etc.) → Supported; network drives → null; DetectedLockSupport overrides (13 cases)
CLockDirectoryFactory.DetectLockSupport All four live-probe outcomes: Unsupported, three variants of ExclusiveOnly (broken shared, broken exclusive-blocks-shared, broken shared-blocks-exclusive), and Supported (5 cases)
DLockDirectoryFactory.Create 10 scenario tests: preferred already Supported; preferred null → detected Supported before temps inspected (preferred over a Supported temp); preferred UnsupportedSupported temp used; preferred null → detected Supported before ExclusiveOnly temps inspected; both preferred and temp are ExclusiveOnly → stay on preferred; temp ExclusiveOnly + preferred Unsupported → use temp; nothing works → Unsupported on preferred; empty drive table → live detection runs (succeeds with fully-supported lock service); temp with no associated drive skipped; first Supported temp wins when multiple candidates
EMountedDrives.GetAssociatedDrive 7 tests: symlink resolution before matching; case-sensitive rejection; case-insensitive match; normalisation; longest-prefix wins (POSIX); DirectoryNotFoundException when no match; ancestor symlink resolved when child doesn't exist
FDefaultPathResolutionService 3 real-filesystem integration tests (2 Unix-only): existing symlink, non-existent child under symlink ancestor, fully non-existent path
GPathResolutionServiceExtensions.ResolvePath 10 tests: non-existent path, existing non-symlink, symlink followed, ancestor symlink + tail re-attached, multiple tail segments re-attached, path normalisation via GetFullPath, all GetFullPath exception types caught (5 parameterised)

IsolationFixture — integration tests via Autofac container

The fixture builds a real IContainer using ScriptIsolationModule and exercises IScriptIsolationEnforcer end-to-end against the local filesystem.

Tests What is tested
10 integration tests Enforce/EnforceAsync with valid options acquires a lock file; releases on dispose; throws LockRejectedException on contention; allows concurrent shared locks; waits for lock release; enforces timeout and throws after it elapses

LockOptionsFixture — unit tests for factory classes

Tests What is tested
RequestedLockOptionsFactory tests Returns null for missing/whitespace Level, MutexName, or TentacleHome; maps FullIsolationExclusive and NoIsolationShared; case-insensitive level parsing; defaults timeout to Infinite on invalid or missing input; throws on invalid mutex name chars
LockOptionsFactory tests CreateLockOptionsOrNull_BuildsCorrectLockFileName asserts on LockFile.File.Name; UseExclusiveIfSharedIsNotSupported — 5 cases: Supported→original returned; ExclusiveOnly+Exclusive→original returned, no warning; ExclusiveOnly+Shared→promoted to Exclusive + warning; Unsupported+Exclusive→null + warning; Unsupported+Shared→null + warning
Pipeline timeout test Fake-time test verifying the Polly pipeline honours a timeout > 1 day

What is not covered by unit tests

  • The end-to-end path through LockDirectoryFactory.Create against a real NFS or FUSE mount. The live probing logic is verified against a real (local) filesystem via IsolationFixture and Group F in LockDirectoryFixture, but the specific scenario of a network filesystem detecting as Unsupported and falling back to /tmp is exercised only through the fakes in Group D.
  • Windows-specific temp directory candidates (%LOCALAPPDATA%\Calamari, %TEMP%) are not covered by integration tests.

@rhysparry rhysparry force-pushed the rhys/eft-383/graceful-lock-fallback branch from dea3c2a to 9ce1b18 Compare March 20, 2026 00:32
Copy link
Copy Markdown
Contributor

@gb-8 gb-8 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've had a scan of the PR and while the approach seems good, and the test coverage seems good, I did find the code hard to follow.
I had to really work to follow the logic through, and see exactly where the determination of locking support was performed.
The logic is all hidden inside things that don't make it clear what they are doing.
Once you are happy that the functionality is all correct, then I'd like to pair on seeing if we can refactor to make it more readable and understandable.


public enum LockCapability
{
Unknown,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is good practice to set the default value explicitly to 0.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (Unsupported is now the default). I was toying with the idea of using [Flags], but I didn't see much benefit to it.

@rhysparry rhysparry force-pushed the rhys/eft-383/graceful-lock-fallback branch 3 times, most recently from 1ccc39e to d873503 Compare March 23, 2026 05:01
Base automatically changed from rhys/eft-251/script-isolation to main March 23, 2026 22:50
@rhysparry rhysparry force-pushed the rhys/eft-383/graceful-lock-fallback branch from 66e4146 to 8480e7e Compare March 23, 2026 23:51
@rhysparry rhysparry force-pushed the rhys/eft-383/graceful-lock-fallback branch from 8480e7e to ac6d515 Compare March 24, 2026 02:45
@rhysparry rhysparry marked this pull request as ready for review March 24, 2026 03:09
@gb-8
Copy link
Copy Markdown
Contributor

gb-8 commented Mar 24, 2026

Notes from our discussion:


How we are actually solving the isolation:

Possible support levels:

  • full (exclusive and shared)
  • partial (exclusive only)
  • none

Possible requested isolation levels:

  • isolated (needs exclusive lock)
  • not-isolated (needs shared lock - to help out other uses)

For phase two, we have a known support level, and a requested isolation level.

Algorithm:

  • If support level is full, return requested isolation level.
  • If support level is partial:
    • always return isolated
    • warn if this is different from request.
  • If support level is none:
    • we cannot do any isolation, so we should warn.
    • lets add alerting so we can see how common this is (might require service message).

Expected implementation

Get the requested locking data:
- requested isolation level
- preferred lockfile folder
- mutex / lockfile name
- timeouts

  1. Find the best lock location (drive or folder)

  2. Determine actual isolation level to use (as we may need to escalate from requested level)

  3. Actually acquire the lock

Thoughts on refactoring:

  • Breaking down the whole process in a hierarchy where sibling actions are at a similar level of abstraction.
    • Avoiding nesting high-level concerns inside other things.
  • Move IO into types which make it clear to expect IO inside them, and separate I/O from other logic.
  • I personally find putting logic in static methods on data-holding types to be unexpected, if that logic is not simple in-memory dat munging.
    • Static factory methods are good, when they don't have any real logic (i.e. stuff you'd be happy to have in a constructor).
    • Anything more complex is better in a separate builder type or factory.

…quire delegate

CachedDriveInfo is extracted from its nested position inside LockDirectory
into its own file. DetectLockSupport gains an overload that accepts a
Func<LockOptions, ILockHandle> delegate in place of the direct FileLock.Acquire
call, making the four lock-probe tests (exclusive, shared, exclusive-blocks-shared,
shared-blocks-exclusive) unit-testable without real filesystem operations.
The existing public overload forwards to FileLock.Acquire.
MountedDrives is extracted from its nested position inside LockDirectory
into its own file with no logic changes.
The public GetLockDirectory(string) now delegates to an internal overload
that accepts a MountedDrives instance and an optional acquire delegate.
This allows tests to supply controlled drive lists and lock-acquisition
behaviour without real filesystem probing.

The refactor also closes a design gap: GetAssociatedDrive calls that
throw DirectoryNotFoundException (e.g. when MountedDrives is empty or a
path does not sit under any known drive root) are now caught, and the
affected path is treated as having no known capability rather than
propagating the exception.
The fallback decision logic (routing between IsFullySupported, IsSupported,
PromoteToExclusiveLock, and returning null) is separated into an internal
static ResolveLockOptions(LockOptions, bool) method. PrepareLockOptions
becomes a thin wrapper that builds the options, logs, then delegates.
This makes all fallback branches unit-testable by constructing a LockOptions
with a controlled LockCapability, without any filesystem access.
…metic tests

GetLockDirectory tests against non-existent paths (e.g. /home/octopus/tentacle)
were failing because DetectLockSupport called Directory.CreateDirectory
unconditionally before probing, causing the outer catch to return Unsupported
instead of running the fake lock service. Thread a createDirectory Action through
GetLockDirectory and DetectLockSupport (defaulting to the real implementation),
and pass a no-op in the two affected Group D fixture tests.
The two delegates (acquireDelegate and createDirectory) passed through
GetLockDirectory and DetectLockSupport were always used together and
represented a single concern: interacting with the filesystem during
lock-support probing. Introducing IFileLockService makes that coupling
explicit and gives tests a single seam to inject.

- Add IFileLockService with AcquireLock and CreateDirectory
- Add FileLockService (singleton) as the real implementation
- Collapse the multi-overload chain in CachedDriveInfo.DetectLockSupport
  and LockDirectory.GetLockDirectory to a single IFileLockService parameter
- FakeLockService in LockDirectoryFixture now implements IFileLockService
  (CreateDirectory is a no-op; Acquire renamed to AcquireLock)
…oryFallback

Move GetTemporaryCandidates logic into a new ITemporaryDirectoryFallback interface
with real implementation TemporaryDirectoryFallback (singleton). This allows the
internal GetLockDirectory overload to accept an injectable fallback, making tests
hermetic and independent of environment variables ($TMPDIR) and filesystem
existence checks (/tmp, /dev/shm).

Also fix bug: the /dev/shm candidate was incorrectly joined under /tmp instead of
/dev/shm.

Add FakeTemporaryDirectoryFallback to tests returning a fixed list of candidates.
All Group D tests now inject this fake, removing any dependency on $TMPDIR, /tmp,
and /dev/shm.

Add 2 new Group D tests:
- Verify temp candidates with no matching drive are silently skipped
- Verify multiple temp candidates: the first Supported one is chosen

All 89 tests pass.
Consolidate ITemporaryDirectoryFallback into IFileLockService by adding
GetFallbackTemporaryDirectories. This removes the separate interface and its
injectable seam, simplifying the design: callers only need to inject one
service. TemporaryDirectoryFallback becomes a static helper used exclusively
by FileLockService. Tests updated to use FakeLockService with temp directory
params instead of the now-deleted FakeTemporaryDirectoryFallback.
@rhysparry rhysparry force-pushed the rhys/eft-383/graceful-lock-fallback branch from 190a711 to 9eeeb25 Compare March 25, 2026 05:33

namespace Calamari.Common.Features.Processes.ScriptIsolation;

public sealed class LockOptionsFactory(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This factory has a bunch of logic and intelligence in it, so maybe should be renamed to communicate that it is doing more than simple constructions.

E.g. LockOptionResolver or something.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renaming to LockOptionsResolver

return UseExclusiveIfSharedIsNotSupported(lockOptions);
}

internal LockOptions? UseExclusiveIfSharedIsNotSupported(LockOptions lockOptions)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can also "downgrade" rather than "escalate" the lock you actually get, so a better name would be:

DetermineActualKindOfLockWeWillUseBasedOnSupport (or something better).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renaming to DetermineActualLockTypeToUseBasedOnSupport and refactoring to make things clearer.

.Build();
}

LockOptions? PrepareLockOptions(CommonOptions.ScriptIsolationOptions scriptIsolationOptions)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better (but not great) name:
DetermineWhatLockToUseBasedOnOptions

The key thing to communicate is that we are making decisions inside here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up with DeterminLockOptionsToEnforceBasedOnIsolationOptions

ILog log
)
{
public RequestedLockOptions? CreateOrNull(CommonOptions.ScriptIsolationOptions options)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have settled on:
CreateFromIsolationOptions()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will rename to CreateFromIsolationOptions

Copy link
Copy Markdown
Contributor

@gb-8 gb-8 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Pre-approval, with various renaming suggested, and some a suggested refactor of LockOptionsFactory.UseExclusiveIfSharedIsNotSupported.

We will likely want to write out service messages each time we escalate/deescalate level of locking do we can write warnings in server logs that we can monitor or alert on. That can go into this PR too or a follow up.

- Rename UseExclusiveIfSharedIsNotSupported to DetermineActualLockTypeToUseBasedOnSupport for better semantic clarity
- Extract lock type support checks into explicit local variables (isExclusiveLockSupported, isSharedLockSupported)
- Restructure conditionals to separate exclusive vs shared lock type handling paths
- Add comments explaining the escalation behavior when shared locks are unavailable
- Extract repeated warning message into LogUnableToSupportAnyScriptIsolation helper method
- Update test references to use the renamed method

This refactoring improves code readability without changing the functional behavior.
…nOptions

Improves method naming to better express the semantic intent: the method creates
requested lock options specifically from script isolation options. This clarifies
the relationship between input and output compared to the generic CreateOrNull name.
Copy link
Copy Markdown
Contributor Author

@rhysparry rhysparry left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just want to break out the service message details first because I'd like to enrich the message with details of what we tried.


namespace Calamari.Common.Features.Processes.ScriptIsolation;

public sealed class LockOptionsFactory(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renaming to LockOptionsResolver

return UseExclusiveIfSharedIsNotSupported(lockOptions);
}

internal LockOptions? UseExclusiveIfSharedIsNotSupported(LockOptions lockOptions)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renaming to DetermineActualLockTypeToUseBasedOnSupport and refactoring to make things clearer.

ILog log
)
{
public RequestedLockOptions? CreateOrNull(CommonOptions.ScriptIsolationOptions options)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will rename to CreateFromIsolationOptions

.Build();
}

LockOptions? PrepareLockOptions(CommonOptions.ScriptIsolationOptions scriptIsolationOptions)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up with DeterminLockOptionsToEnforceBasedOnIsolationOptions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants