FileLockImpl:
MONITOR
The module monitor serializes updates to the LockMode in the FileInstance, and serves no other purpose.
IMPORTS FileInstance, Lock
EXPORTS FileLock, FileLog =
BEGIN OPEN FileLock, FileLockFormat;
VolumeID: TYPE = AlpineEnvironment.VolumeID;
FileID: TYPE = AlpineEnvironment.FileID;
LockIDRep: TYPE = RECORD [v: VolumeID, f: INT, fSub: FileLockSubID];
MakeLockID:
PROC [file: AlpineInternal.FileInstanceHandle, fileLockSubID: FileLockSubID]
RETURNS [lockID: AlpineInternal.LockID] = {
vID: VolumeID; fID: FileID;
[vID, fID] ← FileInstance.GetVolumeIDAndFileID[file];
RETURN [[LOOPHOLE[vID], LOOPHOLE[fID.id], LOOPHOLE[fileLockSubID]]]
};
FileLock.
AcquireFileLock:
PUBLIC
PROCEDURE [fileInstance: FileInstance.Handle, requested: LockOption, minimum: LockMode] =
BEGIN
newFileMode: LockMode;
requested.mode ← Lock.Sup[requested.mode][minimum];
Acquire the file lock unconditionally, regardless of whether or not it is needed, so that calls to Set and Release balance properly.
newFileMode ← Lock.Set[
trans: FileInstance.GetTransHandle[fileInstance],
lock: MakeLockID[fileInstance, [file[]]],
mode: requested.mode, wait: requested.ifConflict=wait];
SetLockMode[fileInstance, newFileMode];
END;
ReleaseFileLock:
PUBLIC
PROCEDURE [fileInstance: FileInstance.Handle] =
BEGIN
Note that in the following call we do not catch Lock.Error. It is a caller error if the lock doesn't exist, and the unreleasable error cannot occur since we specify releasable: ALL in the call.
UnlockAndSetResultingLockMode[
fileInstance: fileInstance,
lockID: MakeLockID[fileInstance, [file[]]],
releasable: ALL[yes]];
END;
AcquirePageLocks:
PUBLIC
PROCEDURE [fileInstance: FileInstance.Handle, pageRun: PageRun, requested: LockOption, minimum: AssertedPageLock] =
BEGIN
existingFileMode: LockMode = FileInstance.GetLockMode[fileInstance];
IF requested.mode NOT IN AssertedPageLock THEN requested.mode ← minimum;
requested.mode ← Lock.Sup[requested.mode][minimum];
If file lock subsumes page lock, then no need to lock page at all.
IF Lock.Sup[requested.mode][existingFileMode] # existingFileMode
THEN
BEGIN
minFileMode: LockMode = minIntention[requested.mode];
IF Lock.Sup[minFileMode][existingFileMode] # existingFileMode
THEN
Must first upgrade file intention lock to cover page lock.
AcquireFileLock[fileInstance: fileInstance, requested: [minFileMode, requested.ifConflict], minimum: minFileMode];
Now lock the page(s) themselves.
FOR page: PageNumber
IN [pageRun.firstPage .. pageRun.firstPage+pageRun.count)
DO
[] ← Lock.Set[
trans: FileInstance.GetTransHandle[fileInstance],
lock: MakeLockID[fileInstance, [page[page]]],
mode: requested.mode, wait: requested.ifConflict=wait !
Lock.Failed =>
-- Attempt to release the locks we already got (if possible)
IF page#pageRun.firstPage
THEN
ReleasePageLocks[fileInstance: fileInstance, pageRun: [firstPage: pageRun.firstPage, count: page-pageRun.firstPage], mode: requested.mode]];
ENDLOOP;
END;
END;
ReleasePageLocks:
PUBLIC
PROCEDURE [
fileInstance: FileInstance.Handle, pageRun: PageRun, mode: LockMode ← read] =
BEGIN
releasable: Lock.ModeReleasableSet ← ALL [no];
releasable[mode] ← yes;
FOR page: PageNumber
IN [pageRun.firstPage .. pageRun.firstPage+pageRun.count)
DO
[] ← Lock.Release[
trans: FileInstance.GetTransHandle[fileInstance],
lock: MakeLockID[fileInstance, [page[page]]],
releasable: releasable !
Lock.Error => CONTINUE];
ENDLOOP;
END;
AcquirePropertyLock:
PUBLIC
PROCEDURE [fileInstance: FileInstance.Handle, property: Property, requested: LockOption, minimum: AssertedPageLock] =
BEGIN
existingFileMode: LockMode = FileInstance.GetLockMode[fileInstance];
IF requested.mode NOT IN AssertedPageLock THEN requested.mode ← minimum;
requested.mode ← Lock.Sup[requested.mode][minimum];
If file lock subsumes property lock, then no need to lock property at all.
IF Lock.Sup[requested.mode][existingFileMode] # existingFileMode
THEN
BEGIN
minFileMode: LockMode = minIntention[requested.mode];
IF Lock.Sup[minFileMode][existingFileMode] # existingFileMode
THEN
Must first upgrade file intention lock to cover property lock.
AcquireFileLock[fileInstance: fileInstance, requested: [minFileMode, requested.ifConflict], minimum: minFileMode];
Now lock the property itself. This is the place where all properties besides version are mapped to a single lock.
[] ← Lock.Set[
trans: FileInstance.GetTransHandle[fileInstance],
lock: MakeLockID[fileInstance, [property[property: IF property=version THEN version ELSE other]]],
mode: requested.mode, wait: requested.ifConflict=wait];
END;
END;
ReleasePropertyLock:
PUBLIC
PROCEDURE [fileInstance: FileInstance.Handle, property: Property, mode: LockMode ← read] =
BEGIN
releasable: Lock.ModeReleasableSet ← ALL [no];
releasable[mode] ← yes;
[] ← Lock.Release[
trans: FileInstance.GetTransHandle[fileInstance],
lock: MakeLockID[fileInstance, [property[property: IF property=version THEN version ELSE other]]],
releasable: releasable !
Lock.Error => CONTINUE];
END;
AcquireSizeLock:
PUBLIC
PROCEDURE [fileInstance: FileInstance.Handle, requested: LockOption, minimum: AssertedPageLock] =
BEGIN
existingFileMode: LockMode = FileInstance.GetLockMode[fileInstance];
IF requested.mode NOT IN AssertedPageLock THEN requested.mode ← minimum;
requested.mode ← Lock.Sup[requested.mode][minimum];
If file lock subsumes size lock, then no need to lock size at all.
IF Lock.Sup[requested.mode][existingFileMode] # existingFileMode
THEN
BEGIN
minFileMode: LockMode = minIntention[requested.mode];
IF Lock.Sup[minFileMode][existingFileMode] # existingFileMode
THEN
Must first upgrade file intention lock to cover size lock.
AcquireFileLock[fileInstance: fileInstance, requested: [minFileMode, requested.ifConflict], minimum: minFileMode];
Now lock the size itself.
[] ← Lock.Set[
trans: FileInstance.GetTransHandle[fileInstance],
lock: MakeLockID[fileInstance, [size[]]],
mode: requested.mode, wait: requested.ifConflict=wait];
END;
END;
AcquireVolumeGroupLock:
PUBLIC
PROCEDURE [volumeGroupID: AlpineEnvironment.VolumeGroupID, trans: TransactionMap.Handle, requested: LockOption, minimum: AssertedPageLock] =
BEGIN
subID: FileLockSubID ← [volumeGroup[]];
IF requested.mode NOT IN AssertedPageLock THEN requested.mode ← minimum;
requested.mode ← Lock.Sup[requested.mode][minimum];
[] ← Lock.Set[
trans: trans,
lock: [LOOPHOLE[volumeGroupID], LOOPHOLE[LONG[0]], LOOPHOLE[subID]],
mode: requested.mode, wait: requested.ifConflict=wait];
END;
Private stuff
minIntention:
PACKED
ARRAY LockMode[none..write]
OF LockMode = [none: none, read: intendRead, write: intendWrite, update: intendUpdate];
-- minIntention[r] is the minimum file intention mode for page lock mode r
This serialization of updates to the FileInstance.LockMode works only under the following assumptions:
1. Setting a lock can only raise the lock's strength or leave it the same; it can never weaken it.
2. It is not harmful for the FileInstance.LockMode to record a lock mode weaker than the one which is actually set on the file.
3. Any given caller will release only those locks which it has set; i.e., it will not call Release more times than it has previously called Acquire.
SetLockMode:
ENTRY
PROCEDURE [fileInstance: FileInstance.Handle, mode: LockMode] =
INLINE
BEGIN
FileInstance.SetLockMode[fileInstance, mode];
END;
UnlockAndSetResultingLockMode:
ENTRY
PROCEDURE [fileInstance: FileInstance.Handle, lockID: AlpineInternal.LockID, releasable: Lock.ModeReleasableSet] =
! Lock.Error {lockUnreleasable, unknown};
BEGIN
ENABLE UNWIND => NULL;
FileInstance.SetLockMode[fileInstance, Lock.Release[trans: FileInstance.GetTransHandle[fileInstance], lock: lockID, releasable: releasable]];
END;
END.
This discussion assumes that there is only one process performing recovery and that actions are recovered in the order that they were originally logged. However, deferred updates may be carried out concurrently, as is true during normal operation.
During recovery, it is necessary in certain cases to set the locks which were set during the original operations. This is necessary if the transaction is in the ready state at the end of recovery, since new actions must be prevented from interfering with the locked entities until the transaction either commits or aborts.
Locking during recovery is not necessary if the recovered state of the transaction is either committed or aborted; in either case, if we did set locks, they would be removed before the end of recovery. Deferred updates have a separate mechanism (the LogMap CheckOut facility) for preventing interference from new actions.
The only locks that really need to be set during recovery are ones corresponding to updates by the recovered transaction. Entities that were merely read by the recovered transaction need not be locked. For such entities, the outcome of the recovered transaction cannot affect the state of the file system as viewed by some new transaction. And updates to such entities by a new transaction cannot affect the state of the file system produced by committing the recovered transaction, because the updates which produce that state are entirely recorded in the log and are not a function of the current state of the base file system.
Therefore, it suffices simply to write-lock all entities which were write-locked originally so as to ensure that no new transaction can access these entities until the outcome of the recovered transaction is assured.
The various types of locks are recovered as follows (bearing in mind that only write locks need to be recovered):
File locks are logged explicitly. This is because opening a file may set a lock but does not otherwise give rise to a logged update. If a whole-file lock existed during the original transaction then it is desirable for it to be set during recovery; this way, page-level locking will be suppressed as it was originally. (This is not logically necessary; but setting more locks during recovery than during the original transaction could break the Lock manager.)
Page locks are recovered by setting them during the processing of WritePages log records; they are set only when not subsumed by a file lock, as just described. Note that writes above highWaterMark (which don't give rise to WritePages log records) are covered by a whole-file lock, because the highWaterMark mechanism only operates when the file is locked.
Size locks are recovered by setting them during the processing of SetSize log records.
Property locks are recovered by setting them during the processing of WriteLeaderPage log records. More precisely, processing a WriteLeaderPage log record sets a property lock on the version number; this is adequate because the version lock covers updating of all properties during commit (see notes in LeaderPageImpl for details).
If everything is working properly, setting a lock during recovery cannot cause a conflict. The locks set during recovery are a subset of the locks set originally. Any lock set during recovery is applied in a context which includes a subset of the original locks, set in the same sequence. Since the original lock sequence worked and locks modes form a partial order, the new lock cannot cause a conflict. Therefore, as a consistency check, all locks are set with ifConflict=fail and Lock.Failed is not caught.
A complication arises if we allow write locks which have been set to be released later while the transaction is still in progress. In principle, this would require us to log releasing locks as well as setting them; not doing so might result in a conflict during recovery. In practice, file locks are released only as part of error recovery within the operation which set them (currently just Open); so we simply arrange not to log the lock until all possibility of error has passed. All other write locks (page, size, property) cannot be released at all.