Loose ends:
1. When we get around to implementing the resumable signal PossiblyDamaged, the catch of ANY in EstablishTransactionContext will need to be reconsidered.
2. If client can predict what OpenFileID will be returned by Open or Create, he may be able to sneak in by virtue of the temporary access provided for AccessControl operations. Is this worth fixing? If so, how?
3. Similarly, if client can predict what FileID will be generated by Create, he may be able to do an Open that references the leader page before it has been initialized, with undefined results.
4. Should we prevent a client from setting locks inconsistent with the client's access to the file (e.g., write locks on a read-only file, or any locks on a read-protected file)? Permitting this enables a client to tie up an arbitrary file with locks and thereby interfere with other clients' access.
5. Create needs to restrict file types.
AlpineFile.
Open:
PUBLIC
PROCEDURE [conversation: Conversation, transID: TransID, universalFile: UniversalFile, access: AccessRights ← readOnly, lock: LockOption ← [intendRead, wait], recoveryOption: RecoveryOption ← log, referencePattern: ReferencePattern ← random]
RETURNS [openFileID: OpenFileID, fileID: FileID] =
BEGIN
Work: FilePrivate.TransactionWork
--[trans, pUpdateCost]-- =
BEGIN
openFile: OpenFileMap.Handle;
fileInstance: FileInstance.Handle;
file: FileMap.Handle;
actualVolumeID: VolumeID;
description: LogMap.FileDescription;
stage: {nothing, handleRegistered, fileLocked} ← nothing;
nameOwnerProperties: LIST OF AlpineEnvironment.PropertyValuePair; -- holds file name and owner name
fileName: Rope.ROPE; -- gets the name from nameOwnerProperties
fileOwner: Rope.ROPE; -- gets the owner from nameOwnerProperties
nameOwnerPropertyList: AlpineFile.PropertySet;
BEGIN
ENABLE {
-- Extra nesting so above variables accessible in
ENABLE and
EXITS clauses
PossiblyDamaged => NULL;
UNWIND, AccessFailed, OperationFailed, StaticallyInvalid, Unknown => {
IF stage>=fileLocked THEN FileLock.ReleaseFileLock[fileInstance];
IF stage>=handleRegistered THEN OpenFileMap.Unregister[openFile];
stage ← nothing;
}};
BEGIN -- More extra nesting so ENABLE in force during EXITS clause
IF access NOT IN AccessRights OR lock.mode NOT IN AlpineEnvironment.LockMode OR recoveryOption NOT IN RecoveryOption OR referencePattern NOT IN ReferencePattern THEN ERROR StaticallyInvalid;
[openFile, openFileID] ← OpenFileMap.Register[conversation: conversation, trans: trans, volumeID: universalFile.volumeID, fileID: universalFile.fileID];
stage ← handleRegistered;
fileInstance ← OpenFileMap.GetFileInstanceHandle[openFile];
file ← FileInstance.GetFileHandle[fileInstance];
Ensure that volumeID identifies an actual volume (as opposed to a volume group), and read-lock the volume group as a side-effect. (Read-locking the volume group is required before several of the AccessControl operations that can be performed on open files, though it is not required during Open itself.)
[volumeID: actualVolumeID] ← VolumeGroup.Identify[volID: universalFile.volumeID, trans: trans, lock: [read, lock.ifConflict] !
VolumeGroup.Failed => GOTO unknownVolumeID];
IF universalFile.volumeID#actualVolumeID THEN GOTO unknownVolumeID;
Acquire file lock before calling AccessControl, because it must be able to read the leader page via this OpenFileID. Also this ensures that the file won't disappear out from under us once we have determined that it in fact exists.
FileLock.AcquireFileLock[fileInstance: fileInstance, requested: lock, minimum: intendRead];
stage ← fileLocked;
description ← LogMap.DescribeFile[file, trans];
IF description.registered AND ~description.exists THEN GOTO unknownFileID;
OpenFileMap.SetAccessRights[openFile, readOnly];
LeaderPage.Validate[fileInstance !
LeaderPage.Error => GOTO damagedLeaderPage];
IF ~AccessControl.PermissionToAccessFile[conversation: conversation, transHandle: trans, openFileID: openFileID, requestedAccess: access, lockOption: lock]
THEN
ERROR AccessFailed[IF access=readOnly THEN fileRead ELSE fileModify];
OpenFileMap.SetAccessRights[openFile, access];
OpenFileMap.SetRecoveryOption[openFile, recoveryOption];
OpenFileMap.SetReferencePattern[openFile, referencePattern];
Record the file's human-legible name. We can't do this when we register the FileMap.Handle (the information isn't available then), so we might as well wait until we can use GetPropertyList. If the stringName is NIL, then we record the owner name to get some grasp of what the file is.
nameOwnerPropertyList[owner] ← nameOwnerPropertyList[stringName] ← TRUE;
nameOwnerProperties ← LeaderPage.GetPropertyList[fileInstance: fileInstance, desiredProperties: nameOwnerPropertyList];
WHILE nameOwnerProperties #
NIL
DO
WITH nameOwnerProperties.first
SELECT
FROM
stringName => fileName ← stringName;
owner => fileOwner ← owner;
ENDCASE => ERROR;
nameOwnerProperties ← nameOwnerProperties.rest;
ENDLOOP;
IF fileName = NIL THEN fileName ← Rope.Cat["(unnamed file owned by ", fileOwner, ")"];
FileMap.SetName[file, fileName];
Must log the setting of the lock, since opening a file does not give rise to any log records which would cause the lock to be set during recovery. Note: do not do this until all possibility of error has passed.
IF lock.mode
IN [update..write]
THEN
FileLog.LogFileLock[fileInstance: fileInstance];
fileID ← universalFile.fileID;
EXITS
damagedLeaderPage => {
logProc: PROC [SkiPatrolLog.OpFailureInfo];
IF (logProc ← SkiPatrolLog.notice.operationFailed) #
NIL
THEN
logProc[[
what: damagedLeaderPage,
transID: transID,
where: "FileActionsImpl.Open",
message: ""
]];
ERROR OperationFailed[damagedLeaderPage];
};
unknownFileID => ERROR Unknown[fileID];
unknownVolumeID => ERROR Unknown[volumeID];
END;
END;
END; -- Work
FilePrivate.EstablishTransactionContext[conversation, transID, Work];
END;
Create:
PUBLIC
PROCEDURE [conversation: Conversation, transID: TransID, volumeID: VolOrVolGroupID, owner: OwnerName, initialSize: PageCount, recoveryOption: RecoveryOption ← log, referencePattern: ReferencePattern ← random]
RETURNS [openFileID: OpenFileID, universalFile: UniversalFile] =
BEGIN
Work: FilePrivate.TransactionWork
--[trans, pUpdateCost]-- =
BEGIN
openFile: OpenFileMap.Handle;
fileInstance: FileInstance.Handle;
file: FileMap.Handle;
volumeGroupID: AlpineEnvironment.VolumeGroupID;
stage: {nothing, spaceChanged, handleRegistered} ← nothing;
BEGIN
ENABLE {
-- Extra nesting so above variables accessible in
ENABLE and
EXITS clauses
PossiblyDamaged => NULL;
UNWIND, AccessFailed, OperationFailed, StaticallyInvalid, Unknown => {
IF stage >= handleRegistered THEN OpenFileMap.Unregister[openFile];
IF stage >= spaceChanged
THEN
IF ~AccessControl.ChangeSpaceViaOwner[transHandle: trans, volGroupID: volumeGroupID, ownerName: owner, nPages: -(initialSize+1)] THEN ERROR;
stage ← nothing;
}};
BEGIN -- More extra nesting so ENABLE in force during EXITS clause
IF owner=
NIL
OR initialSize
NOT
IN [0..AlpineEnvironment.maxPagesPerFile]
OR
--type
NOT IN [ ... legal Alpine file types ... ]
OR-- recoveryOption
NOT
IN RecoveryOption
OR referencePattern
NOT
IN ReferencePattern
THEN
ERROR StaticallyInvalid;
Note: as a side-effect, this read-locks the volume group, which is required by several of the AccessControl procedures which we call subsequently.
[volumeID: universalFile.volumeID, volumeGroupID: volumeGroupID] ← VolumeGroup.Identify[volID: volumeID, trans: trans, lock: [read, fail] !
VolumeGroup.Failed => GOTO unknownVolumeID];
IF ~AccessControl.PermissionToCreateFilesForThisOwner[conversation: conversation, transHandle: trans, volGroupID: volumeGroupID, ownerName: owner] THEN ERROR AccessFailed[ownerCreate];
IF ~AccessControl.ChangeSpaceViaOwner[transHandle: trans, volGroupID: volumeGroupID, ownerName: owner, nPages: initialSize+1] THEN ERROR AccessFailed[spaceQuota];
stage ← spaceChanged;
IF universalFile.volumeID=AlpineEnvironment.nullVolumeID
THEN
universalFile.volumeID ← VolumeGroup.SelectVolumeForCreate[volumeGroupID, initialSize+1 !
VolumeGroup.Failed => GOTO insufficientSpace];
{
Create1:
SAFE
PROC [fileID: AlpineEnvironment.FileID] =
TRUSTED {
IF stage = handleRegistered
THEN {
-- cleanup volatile structures from last call
OpenFileMap.Unregister[openFile];
stage ← spaceChanged;
};
universalFile.fileID ← fileID;
[openFile, openFileID] ← OpenFileMap.Register[conversation: conversation, trans: trans, volumeID: universalFile.volumeID, fileID: fileID];
stage ← handleRegistered;
fileInstance ← OpenFileMap.GetFileInstanceHandle[openFile];
file ← FileInstance.GetFileHandle[fileInstance];
FileLock.AcquireFileLock[fileInstance: fileInstance, requested: [write, fail], minimum: write];
FileLog.LogCreate[fileInstance: fileInstance, initialSize: initialSize, owner: owner];
};
FilePageMgr.Create[
volumeID: universalFile.volumeID, initialSize: initialSize, proc: Create1 !
FilePageMgr.VolumeTooFragmented => GOTO volumeFragmented;
FilePageMgr.InsufficientSpaceOnVolume => GOTO insufficientSpace];
LogMap.RegisterCreate[file: file, trans: trans];
};
pUpdateCost^ ← 20 + initialSize/5; -- Creating a file has a substantial fixed overhead and a relatively small additional cost proportional to the initial size of the file
OpenFileMap.SetAccessRights[openFile, readWrite];
OpenFileMap.SetRecoveryOption[openFile, recoveryOption];
OpenFileMap.SetReferencePattern[openFile, referencePattern];
[] ← FileInstance.SetHighWaterMark[fileInstance, 0];
LogMap.SetCommittedHighWaterMark[file, 0];
LeaderPage.Initialize[fileInstance];
LeaderPage.SetProperty[fileInstance: fileInstance, propertyValue: [owner[owner]]];
FOR access: AccessRights
IN AccessRights
DO
accessList: AccessList = AccessControl.GetDefaultForFileAccessList[transHandle: trans, volGroupID: volumeGroupID, ownerName: owner, accessListType: access];
LeaderPage.SetProperty[fileInstance: fileInstance, propertyValue: IF access=readOnly THEN [readAccess[accessList]] ELSE [modifyAccess[accessList]]];
ENDLOOP;
EXITS
unknownVolumeID => ERROR Unknown[volumeID];
volumeFragmented =>
{
logProc: PROC [SkiPatrolLog.OpFailureInfo];
IF (logProc ← SkiPatrolLog.notice.operationFailed) #
NIL
THEN
logProc[[
what: volumeFragmented,
transID: transID,
where: "FileActionsImpl.Create",
message: ""
]];
ERROR OperationFailed[volumeFragmented];
};
insufficientSpace => {
logProc: PROC [SkiPatrolLog.OpFailureInfo];
IF (logProc ← SkiPatrolLog.notice.operationFailed) #
NIL
THEN
logProc[[
what: insufficientSpace,
transID: transID,
where: "FileActionsImpl.Create",
message: ""
]];
ERROR OperationFailed[insufficientSpace];
};
END;
END;
END; -- Work
FilePrivate.EstablishTransactionContext[conversation: conversation, transID: transID, work: Work, workLevel: hard]; -- This must start work with workLevel=hard because it dirties the owner data base
END;
Close:
PUBLIC
PROCEDURE [conversation: Conversation, openFileID: OpenFileID] =
BEGIN
Work: FilePrivate.OpenFileWork
--[openFile, fileInstance, file, trans, pUpdateCost]-- =
BEGIN
OpenFileMap.Unregister[openFile];
END; -- Work
FilePrivate.EstablishOpenFileContext[conversation: conversation, openFileID: openFileID, work: Work, concurrency: exclusive]; -- This must start work with concurrency=exclusive so as to avoid unexpected errors in Delete and SetSize caused by the OpenFileID becoming invalid in mid-execution
END;
Delete:
PUBLIC
PROCEDURE [conversation: Conversation, openFileID: OpenFileID] =
BEGIN
Work: FilePrivate.OpenFileWork
--[openFile, fileInstance, file, trans, pUpdateCost]-- =
BEGIN
uncommittedSize, committedSize: PageCount;
IF OpenFileMap.GetAccessRights[openFile]#readWrite
THEN
ERROR AccessFailed[handleReadWrite];
FileLock.AcquireFileLock[fileInstance: fileInstance, requested: [update, OpenFileMap.GetLockOption[openFile].ifConflict], minimum: update];
FilePrivate.AcquireFileInterlock[file]; -- Do this AFTER acquiring lock so we don't wait with the file interlock set
Credit the file's size (plus leader page) to the owner's space quota. Note that we use the uncommitted size (as viewed from this transaction), since if that differs from the committed size then the transaction must previously have done a SetSize, which already adjusted the space quota by the amount of the change.
committedSize ← LogMap.GetCommittedSize[file];
IF committedSize=LAST[PageCount] THEN committedSize ← FilePageMgr.GetSize[file];
uncommittedSize ← LogMap.GetUncommittedSize[file: file, trans: trans];
IF uncommittedSize=LAST[PageCount] THEN uncommittedSize ← committedSize;
IF ~AccessControl.ChangeSpaceViaOpenFileID[conversation: conversation, transHandle: trans, openFileID: openFileID, nPages: -(uncommittedSize+1)] THEN ERROR;
Ensure that any committed deferred updates to the file have been carried out so as to avoid confusion after commit resulting from attempts to perform updates to a nonexistent file. Note that there cannot be any uncommitted updates for any transaction besides this one, because the file is locked. Uncommitted updates for this transaction are no problem because they are simply ignored at commit time. Note: if this transaction previously shortened the file, pages beyond uncommittedSize were already carried out by SetSize. Therefore, flush only up to uncommittedSize. This is important because LogMap will raise an error if the pageRun extends beyond uncommittedSize.
FilePrivate.PerformAnyCommittedIntentions[fileInstance: fileInstance, pageRun: [firstPage: 0, count: uncommittedSize]];
Now actually register the delete intention.
FileLog.LogDelete[fileInstance];
LogMap.RegisterDelete[file: file, trans: trans];
FileInstance.SetMaxDeltaVersion[fileInstance, 1]; -- note update
OpenFileMap.Unregister[openFile];
END; -- Work
FilePrivate.EstablishOpenFileContext[conversation: conversation, openFileID: openFileID, work: Work, workLevel: hard]; -- This must start work with workLevel=hard because it dirties the owner data base. Start with concurrency=normal until locks have been set, then upgrade to concurrency=exclusive so as to avoid inconsistencies in changing the space quota.
END;
GetUniversalFile:
PUBLIC
PROCEDURE [conversation: Conversation, openFileID: OpenFileID]
RETURNS [universalFile: UniversalFile] =
BEGIN
Work: FilePrivate.OpenFileWork
--[openFile, fileInstance, file, trans, pUpdateCost]-- =
BEGIN
universalFile.volumeID ← FileMap.GetVolumeID[file];
universalFile.fileID ← FileMap.GetFileID[file];
END; -- Work
FilePrivate.EstablishOpenFileContext[conversation, openFileID, Work];
END;
GetTransID:
PUBLIC
PROCEDURE [conversation: Conversation, openFileID: OpenFileID]
RETURNS [transID: TransID] =
BEGIN
Work: FilePrivate.OpenFileWork
--[openFile, fileInstance, file, trans, pUpdateCost]-- =
BEGIN
transID ← TransactionMap.GetTransID[FileInstance.GetTransHandle[fileInstance]];
END; -- Work
FilePrivate.EstablishOpenFileContext[conversation, openFileID, Work];
END;
GetAccessRights:
PUBLIC
PROCEDURE [conversation: Conversation, openFileID: OpenFileID]
RETURNS [access: AccessRights] =
BEGIN
Work: FilePrivate.OpenFileWork
--[openFile, fileInstance, file, trans, pUpdateCost]-- =
BEGIN
access ← OpenFileMap.GetAccessRights[openFile];
END; -- Work
FilePrivate.EstablishOpenFileContext[conversation, openFileID, Work];
END;
GetLockOption:
PUBLIC
PROCEDURE [conversation: Conversation, openFileID: OpenFileID]
RETURNS [lock: LockOption] =
BEGIN
Work: FilePrivate.OpenFileWork
--[openFile, fileInstance, file, trans, pUpdateCost]-- =
BEGIN
lock ← OpenFileMap.GetLockOption[openFile];
END; -- Work
FilePrivate.EstablishOpenFileContext[conversation, openFileID, Work];
END;
SetLockOption:
PUBLIC
PROCEDURE [conversation: Conversation, openFileID: OpenFileID, lock: LockOption] =
BEGIN
Work: FilePrivate.OpenFileWork
--[openFile, fileInstance, file, trans, pUpdateCost]-- =
BEGIN
IF lock.mode NOT IN AlpineEnvironment.LockMode THEN ERROR StaticallyInvalid;
This will set the mode to the MAX of lock.mode and its existing mode.
FileLock.AcquireFileLock[fileInstance: fileInstance, requested: lock, minimum: FileInstance.GetLockMode[fileInstance]];
OpenFileMap.SetLockOption[ openFile, [FileInstance.GetLockMode[fileInstance], lock.ifConflict]];
END; -- Work
FilePrivate.EstablishOpenFileContext[conversation, openFileID, Work];
END;
GetRecoveryOption:
PUBLIC
PROCEDURE [conversation: Conversation, openFileID: OpenFileID]
RETURNS [recoveryOption: RecoveryOption] =
BEGIN
Work: FilePrivate.OpenFileWork
--[openFile, fileInstance, file, trans, pUpdateCost]-- =
BEGIN
recoveryOption ← OpenFileMap.GetRecoveryOption[openFile];
END; -- Work
FilePrivate.EstablishOpenFileContext[conversation, openFileID, Work];
END;
GetReferencePattern:
PUBLIC
PROCEDURE [conversation: Conversation, openFileID: OpenFileID]
RETURNS [referencePattern: ReferencePattern] =
BEGIN
Work: FilePrivate.OpenFileWork
--[openFile, fileInstance, file, trans, pUpdateCost]-- =
BEGIN
referencePattern ← OpenFileMap.GetReferencePattern[openFile];
END; -- Work
FilePrivate.EstablishOpenFileContext[conversation, openFileID, Work];
END;
SetReferencePattern:
PUBLIC
PROCEDURE [ conversation: Conversation, openFileID: OpenFileID, referencePattern: ReferencePattern] =
BEGIN
Work: FilePrivate.OpenFileWork
--[openFile, fileInstance, file, trans, pUpdateCost]-- =
BEGIN
IF referencePattern NOT IN ReferencePattern THEN ERROR StaticallyInvalid;
OpenFileMap.SetReferencePattern[openFile, referencePattern];
END; -- Work
FilePrivate.EstablishOpenFileContext[conversation, openFileID, Work];
END;
ReadProperties:
PUBLIC
PROCEDURE [conversation: Conversation, openFileID: OpenFileID, desiredProperties: PropertySet ← allProperties, lock: LockOption ← [read, wait]]
RETURNS [properties:
LIST
OF PropertyValuePair] =
BEGIN
Work: FilePrivate.OpenFileWork
--[openFile, fileInstance, file, trans, pUpdateCost]-- =
BEGIN
IF lock.mode
NOT
IN AlpineEnvironment.LockMode
THEN
ERROR StaticallyInvalid;
properties ← LeaderPage.GetPropertyList[fileInstance: fileInstance, desiredProperties: desiredProperties, lock: lock];
END; -- Work
FilePrivate.EstablishOpenFileContext[conversation, openFileID, Work];
END;
WriteProperties:
PUBLIC
PROCEDURE [conversation: Conversation, openFileID: OpenFileID, properties:
LIST
OF PropertyValuePair, lock: LockOption ← [update, wait]] =
BEGIN
Work: FilePrivate.OpenFileWork
--[openFile, fileInstance, file, trans, pUpdateCost]-- =
BEGIN
IF lock.mode NOT IN AlpineEnvironment.LockMode THEN ERROR StaticallyInvalid;
FOR item:
LIST
OF PropertyValuePair ← properties, item.rest
UNTIL item=
NIL
DO
IF item.first.property NOT IN Property THEN ERROR StaticallyInvalid;
SELECT item.first.property
FROM
modifyAccess, readAccess => NULL;
ENDCASE =>
IF OpenFileMap.GetAccessRights[openFile] # readWrite THEN ERROR AccessFailed[handleReadWrite];
ENDLOOP;
FOR item:
LIST
OF PropertyValuePair ← properties, item.rest
UNTIL item=
NIL
DO
ENABLE
LeaderPage.Error => IF errorType=full THEN GOTO leaderPageFull;
WITH propertyValue: item.first
SELECT
FROM
highWaterMark =>
BEGIN
IF propertyValue.highWaterMark NOT IN [0..AlpineEnvironment.maxPagesPerFile] THEN ERROR StaticallyInvalid;
LeaderPage.SetProperty[fileInstance: fileInstance, propertyValue: propertyValue, lock: lock];
END;
modifyAccess, readAccess =>
BEGIN
IF ~AccessControl.PermissionToModifyFileAccessList[conversation: conversation, transHandle: trans, openFileID: openFileID] THEN ERROR AccessFailed[ownerCreate];
LeaderPage.SetProperty[fileInstance: fileInstance, propertyValue: propertyValue, lock: lock];
END;
owner =>
BEGIN
size: PageCount;
oldOwner: PropertyValuePair[owner];
volumeGroupID: AlpineEnvironment.VolumeGroupID ← VolumeGroup.Identify[volID: FileMap.GetVolumeID[file]].volumeGroupID;
stage: {nothing, ownerChanged, spaceChanged} ← nothing;
BEGIN
ENABLE
UNWIND, AccessFailed => {
IF stage>=ownerChanged
THEN
LeaderPage.SetProperty[fileInstance: fileInstance, propertyValue: oldOwner, lock: lock];
IF stage>=spaceChanged
THEN
IF ~AccessControl.ChangeSpaceViaOwner[transHandle: trans, volGroupID: volumeGroupID, ownerName: propertyValue.owner, nPages: -(size+1)] THEN ERROR;
stage ← nothing};
IF propertyValue.owner=NIL THEN ERROR StaticallyInvalid;
IF ~AccessControl.PermissionToModifyFileAccessList[conversation: conversation, transHandle: trans, openFileID: openFileID] THEN ERROR AccessFailed[ownerCreate];
IF ~AccessControl.PermissionToCreateFilesForThisOwner[conversation: conversation, transHandle: trans, volGroupID: volumeGroupID, ownerName: propertyValue.owner] THEN ERROR AccessFailed[ownerCreate];
size ← AlpineFile.GetSize[conversation: conversation, openFileID: openFileID];
Change owner name in leader page, but remember the old one in case we later need to back out.
oldOwner ← NARROW[LeaderPage.GetProperty[fileInstance: fileInstance, property: owner, lock: [read, lock.ifConflict]], PropertyValuePair[owner]];
IF
NOT Rope.Equal[oldOwner.owner, propertyValue.owner,
FALSE]
THEN {
LeaderPage.SetProperty[fileInstance: fileInstance, propertyValue: propertyValue, lock: lock];
stage ← ownerChanged;
Increase space used by new owner.
IF ~AccessControl.ChangeSpaceViaOwner[transHandle: trans, volGroupID: volumeGroupID, ownerName: propertyValue.owner, nPages: size+1]
THEN
ERROR AccessFailed[spaceQuota];
stage ← spaceChanged;
Decrease space used by old owner.
IF ~AccessControl.ChangeSpaceViaOpenFileID[conversation: conversation, transHandle: trans, openFileID: openFileID, nPages: -(size+1)] THEN ERROR;
};
END;
END;
version => {
logProc: PROC [SkiPatrolLog.OpFailureInfo];
IF (logProc ← SkiPatrolLog.notice.operationFailed) #
NIL
THEN
logProc[[
what: unwritableProperty,
transID: SkiPatrolHooks.FileInfoToTransID[conversation, openFileID],
where: "FileActionsImpl.WriteProperties",
message: "tried to write 'version' property"
]];
ERROR OperationFailed[unwritableProperty];
};
ENDCASE =>
LeaderPage.SetProperty[fileInstance: fileInstance, propertyValue: propertyValue, lock: lock];
REPEAT
leaderPageFull =>
{
logProc: PROC [SkiPatrolLog.OpFailureInfo];
IF (logProc ← SkiPatrolLog.notice.operationFailed) #
NIL
THEN
logProc[[
what: insufficientSpace,
transID: SkiPatrolHooks.FileInfoToTransID[conversation, openFileID],
where: "FileActionsImpl.WriteProperties",
message: ""
]];
ERROR OperationFailed[insufficientSpace];
};
ENDLOOP;
pUpdateCost^ ← 1; -- Need to assign some cost to this update, even though it doesn't actually write anything in the log until commit time
END; -- Work
FilePrivate.EstablishOpenFileContext[conversation, openFileID, Work];
END;
UnlockVersion:
PUBLIC
PROCEDURE [conversation: Conversation, openFileID: OpenFileID] =
BEGIN
Work: FilePrivate.OpenFileWork
--[openFile, fileInstance, file, trans, pUpdateCost]-- =
BEGIN
FileLock.ReleasePropertyLock[fileInstance: fileInstance, property: version];
END; -- Work
FilePrivate.EstablishOpenFileContext[conversation, openFileID, Work];
END;
IncrementVersion:
PUBLIC
PROCEDURE [conversation: Conversation, openFileID: OpenFileID, increment:
LONG
CARDINAL] =
BEGIN
Work: FilePrivate.OpenFileWork
--[openFile, fileInstance, file, trans, pUpdateCost]-- =
BEGIN
IF increment<=0 THEN ERROR StaticallyInvalid;
FileInstance.SetMaxDeltaVersion[fileInstance, increment];
pUpdateCost^ ← 1;
END; -- Work
FilePrivate.EstablishOpenFileContext[conversation, openFileID, Work];
END;
GetSize:
PUBLIC
PROCEDURE [conversation: Conversation, openFileID: OpenFileID, lock: LockOption ← [read, wait]]
RETURNS [size: PageCount] =
BEGIN
Work: FilePrivate.OpenFileWork
--[openFile, fileInstance, file, trans, pUpdateCost]-- =
BEGIN
IF lock.mode NOT IN AlpineEnvironment.LockMode THEN ERROR StaticallyInvalid;
FileLock.AcquireSizeLock[fileInstance: fileInstance, requested: lock, minimum: read];
size ← LogMap.GetUncommittedSize[file, trans]; -- non-nil iff changed by trans
IF size=LAST[PageCount] THEN size ← LogMap.GetCommittedSize[file];
IF size=LAST[PageCount] THEN size ← FilePageMgr.GetSize[file];
END; -- Work
FilePrivate.EstablishOpenFileContext[conversation, openFileID, Work];
END;
SetSize:
PUBLIC
PROCEDURE [conversation: Conversation, openFileID: OpenFileID, size: PageCount, lock: LockOption ← [update, wait]] =
BEGIN
Work: FilePrivate.OpenFileWork
--[openFile, fileInstance, file, trans, pUpdateCost]-- =
BEGIN
committedSize, uncommittedSize, actualSize, highWaterMark: PageCount;
failure: AlpineEnvironment.OperationFailure;
IF lock.mode NOT IN AlpineEnvironment.LockMode OR size NOT IN [0..AlpineEnvironment.maxPagesPerFile] THEN ERROR StaticallyInvalid;
IF OpenFileMap.GetAccessRights[openFile] # readWrite THEN ERROR AccessFailed[handleReadWrite];
FileLock.AcquireSizeLock[fileInstance: fileInstance, requested: lock, minimum: update];
actualSize ← FilePageMgr.GetSize[file];
committedSize ← LogMap.GetCommittedSize[file];
IF committedSize=
LAST[PageCount]
THEN
LogMap.SetCommittedSize[file: file, size: (committedSize ← actualSize)];
IF size<committedSize
THEN
New size less than committed: must acquire whole-file lock.
FileLock.AcquireFileLock[fileInstance: fileInstance, requested: lock, minimum: update];
FilePrivate.AcquireFileInterlock[file]; -- do this AFTER acquiring lock(s) so we don't wait with the file interlock set
uncommittedSize ← LogMap.GetUncommittedSize[file, trans];
IF uncommittedSize=LAST[PageCount] THEN uncommittedSize ← actualSize;
IF ~AccessControl.ChangeSpaceViaOpenFileID[conversation: conversation, transHandle: trans, openFileID: openFileID, nPages: size-uncommittedSize] THEN ERROR AccessFailed[spaceQuota];
If the file is being shortened, ensure that any committed deferred updates to the portion of the file that will be truncated have been carried out so as to avoid confusion after commit resulting from attempts to perform updates to nonexistent pages. Note that there cannot be any uncommitted updates for any transaction besides this one, because the file is locked. Uncommitted updates for this transaction are no problem because they are simply ignored at commit time.
IF size<uncommittedSize
THEN
FilePrivate.PerformAnyCommittedIntentions[fileInstance: fileInstance, pageRun: [firstPage: size, count: uncommittedSize-size]];
Must log intention before lengthening file so that if we crash, recovery will reset size to its original value.
pUpdateCost^ ← 5+ABS[size-uncommittedSize]/5;
FileLog.LogSetSize[fileInstance: fileInstance, old: committedSize, new: size];
FileInstance.SetMaxDeltaVersion[fileInstance, 1]; -- note update
IF size>actualSize
THEN
BEGIN
FilePageMgr.SetSize[fileHandle: file, size: size !
FilePageMgr.VolumeTooFragmented => { failure ← volumeFragmented; GOTO setSizeFailure };
FilePageMgr.InsufficientSpaceOnVolume => { failure ← insufficientSpace; GOTO setSizeFailure }];
EXITS
setSizeFailure => {
logProc: PROC [SkiPatrolLog.OpFailureInfo]; -- (used to prevent race condition)
give back the allocation before raising error
IF ~AccessControl.ChangeSpaceViaOpenFileID[conversation: conversation, transHandle: trans, openFileID: openFileID, nPages: uncommittedSize-size] THEN ERROR;
Must log another SetSize intention to restore file to its original uncommitted size so that if trans runs to completion but we crash during commit, recovery won't think we have to lengthen the file.
FileLog.LogSetSize[fileInstance: fileInstance, old: committedSize, new: uncommittedSize];
IF (logProc ← SkiPatrolLog.notice.operationFailed) #
NIL
THEN
logProc[[
what: failure,
transID: SkiPatrolHooks.FileInfoToTransID[conversation, openFileID],
where: "FileActionsImpl.SetSize",
message: ""
]];
ERROR OperationFailed[failure];
};
END;
LogMap.SetUncommittedSize[file: file, trans: trans, size: size];
If highWaterMark is beyond the new end of the file then reset it to the end.
highWaterMark ← NARROW[LeaderPage.GetProperty[fileInstance: fileInstance, property: highWaterMark, lock: [read, lock.ifConflict]], PropertyValuePair[highWaterMark]].highWaterMark;
IF highWaterMark>size
THEN
LeaderPage.SetProperty[fileInstance: fileInstance, propertyValue: [highWaterMark[highWaterMark]], lock: lock];
END; -- Work
FilePrivate.EstablishOpenFileContext[conversation: conversation, openFileID: openFileID, work: Work, workLevel: hard]; -- This must start work with workLevel=hard because it dirties the owner data base. Start with concurrency=normal until locks have been set, then upgrade to concurrency=exclusive so as to avoid inconsistencies in changing the space quota.
END;
UnlockFile:
PUBLIC
PROCEDURE [conversation: Conversation, openFileID: OpenFileID, retainCacheLocks:
BOOLEAN] =
BEGIN
Work: FilePrivate.OpenFileWork
--[openFile, fileInstance, file, trans, pUpdateCost]-- =
BEGIN
FileLock.ReleaseFile[fileInstance: fileInstance, retainCacheLocks: retainCacheLocks];
END; -- Work
FilePrivate.EstablishOpenFileContext[conversation, openFileID, Work];
END;
UserProperty: TYPE = AlpineEnvironment.UserProperty;
UserPropertyValuePair: TYPE = AlpineEnvironment.UserPropertyValuePair;
UserProperties: TYPE = AlpineEnvironment.UserProperties;
UserPropertyValuePairs: TYPE = AlpineEnvironment.UserPropertyValuePairs;
ReadUserProperties:
PUBLIC PROCEDURE [conversation: Conversation, openFileID: OpenFileID, desiredProperties: UserProperties, lock: LockOption ← [read, wait]]
RETURNS [properties: UserPropertyValuePairs] ~ {
Work: FilePrivate.OpenFileWork
--[openFile, fileInstance, file, trans, pUpdateCost]-- =
BEGIN
IF lock.mode
NOT
IN AlpineEnvironment.LockMode
THEN
ERROR StaticallyInvalid;
properties ← LeaderPage.GetUserPropertyList[fileInstance: fileInstance, desiredProperties: desiredProperties, lock: lock];
END; -- Work
FilePrivate.EstablishOpenFileContext[conversation, openFileID, Work];
};
WriteUserProperties:
PUBLIC PROCEDURE [conversation: Conversation, openFileID: OpenFileID, properties: UserPropertyValuePairs, lock: LockOption ← [write, wait]] ~ {
Work: FilePrivate.OpenFileWork
--[openFile, fileInstance, file, trans, pUpdateCost]-- =
BEGIN
IF lock.mode NOT IN AlpineEnvironment.LockMode THEN ERROR StaticallyInvalid;
IF OpenFileMap.GetAccessRights[openFile] # readWrite THEN ERROR AccessFailed[handleReadWrite];
FOR p: UserPropertyValuePairs ← properties, p.rest
WHILE p #
NIL
DO
ENABLE
LeaderPage.Error => IF errorType=full THEN GOTO leaderPageFull;
LeaderPage.SetUserProperty[fileInstance: fileInstance, propertyValue: p.first, lock: lock];
REPEAT
leaderPageFull =>
{
logProc: PROC [SkiPatrolLog.OpFailureInfo];
IF (logProc ← SkiPatrolLog.notice.operationFailed) #
NIL
THEN
logProc[[
what: insufficientSpace,
transID: SkiPatrolHooks.FileInfoToTransID[conversation, openFileID],
where: "FileActionsImpl.WriteProperties",
message: ""
]];
ERROR OperationFailed[insufficientSpace];
};
ENDLOOP;
pUpdateCost^ ← 1; -- Need to assign some cost to this update, even though it doesn't actually write anything in the log until commit time
END; -- Work
FilePrivate.EstablishOpenFileContext[conversation, openFileID, Work];
};
FileLog.
RecoverCreate:
PUBLIC
PROCEDURE [fileInstance: FileInstance.Handle, initialSize: PageCount, outcome: FileLog.TransState] =
BEGIN
Note: no need to call AccessControl.ChangeSpaceViaOwner again, because the change to the owner data base was logged separately and will therefore be recovered.
file: FileMap.Handle = FileInstance.GetFileHandle[fileInstance];
trans: TransactionMap.Handle = FileInstance.GetTransHandle[fileInstance];
IF outcome=ready
THEN
BEGIN
actualSize: PageCount;
FileLock.AcquireFileLock[fileInstance: fileInstance, requested: [write, fail], minimum: write];
Since trans is not yet committed and files are not shortened until after commit, the file must still be at least initialSize pages long.
actualSize ← FilePageMgr.GetSize[file ! FilePageMgr.NoSuchFile => GOTO ignore];
IF actualSize<initialSize THEN ERROR;
END;
Register the creation unconditionally so that if the transaction eventually aborts the file (if it still exists) will be deleted.
LogMap.RegisterCreate[file: file, trans: trans];
EXITS
ignore => NULL; -- (SkiPatrolLog will ignore this, too)
END;
RecoverDelete:
PUBLIC
PROCEDURE [fileInstance: FileInstance.Handle, outcome: FileLog.TransState] =
BEGIN
file: FileMap.Handle = FileInstance.GetFileHandle[fileInstance];
trans: TransactionMap.Handle = FileInstance.GetTransHandle[fileInstance];
uncommittedSize: PageCount;
IF outcome=ready
THEN
FileLock.AcquireFileLock[fileInstance: fileInstance, requested: [write, fail], minimum: write];
Carry out any committed deferred writes.
uncommittedSize ← LogMap.GetUncommittedSize[file: file, trans: trans];
IF uncommittedSize=LAST[PageCount] THEN uncommittedSize ← LogMap.GetCommittedSize[file];
IF uncommittedSize=LAST[PageCount] THEN uncommittedSize ← FilePageMgr.GetSize[file ! FilePageMgr.NoSuchFile => GOTO alreadyDeleted];
IF uncommittedSize#
LAST[PageCount]
THEN
FilePrivate.PerformAnyCommittedIntentions[fileInstance: fileInstance, pageRun: [firstPage: 0, count: uncommittedSize]];
LogMap.RegisterDelete[file: file, trans: trans];
EXITS
alreadyDeleted => NULL;
END;
RecoverSetSize:
PUBLIC
PROCEDURE [fileInstance: FileInstance.Handle, old, new: PageCount, outcome: FileLog.TransState] =
BEGIN
file: FileMap.Handle = FileInstance.GetFileHandle[fileInstance];
trans: TransactionMap.Handle = FileInstance.GetTransHandle[fileInstance];
actualSize: PageCount = FilePageMgr.GetSize[file ! FilePageMgr.NoSuchFile => GOTO alreadyDeleted];
IF outcome=ready
THEN
BEGIN
FileLock.AcquireSizeLock[fileInstance: fileInstance, requested: [write, fail], minimum: write];
IF new<old
THEN
FileLock.AcquireFileLock[fileInstance: fileInstance, requested: [write, fail], minimum: write];
Since trans is not yet committed and files are not shortened until after commit, the file must still be at least MAX[old, new] pages long. -- the preceding statement is FALSE! A log record may have been written showing the file being lengthened, but the File.SetSize to actually lengthen it never happened. This situation has occurred. -- Hauser, May 22, 1986
IF actualSize<MAX[old, new] THEN ERROR;
END;
SELECT new
FROM
< actualSize =>
Carry out any committed deferred writes to the portion of the file being truncated.
IF new<old
THEN
FilePrivate.PerformAnyCommittedIntentions[fileInstance: fileInstance, pageRun: [firstPage: new, count: old-new]];
> actualSize =>
This can happen if the recovered actions include both an increase and a decrease.
FilePageMgr.SetSize[file, new];
ENDCASE;
Register the change unconditionally so that if the transaction eventually aborts the change will be undone (if necessary).
LogMap.SetCommittedSize[file: file, size: old];
LogMap.SetUncommittedSize[file: file, trans: trans, size: new];
EXITS
alreadyDeleted => NULL;
END;
FilePrivate.
PrepareForCommit:
PUBLIC
PROCEDURE [fileInstance: FileInstance.Handle] =
BEGIN
file: FileMap.Handle = FileInstance.GetFileHandle[fileInstance];
trans: TransactionMap.Handle = FileInstance.GetTransHandle[fileInstance];
description: LogMap.FileDescription = LogMap.DescribeFile[file, trans];
IF FileInstance.GetDeltaVersion[fileInstance]#0
THEN
BEGIN
LeaderPage.Finalize[fileInstance];
As the last act before commit, force out any dirty pages belonging to the base file if the file was created by this transaction or if there were writes above highWaterMark. These must be written stably before commit, since there is no record of them in the log. Such pages cannot exist during recovery; and forcing them out is pointless if the file has been deleted by this transaction.
IF (description.created
OR FileInstance.GetHighWaterMark[fileInstance] > LogMap.GetCommittedHighWaterMark[file])
AND ~FilePrivate.recovering
AND ~description.deleted
THEN
FilePageMgr.ForceOutFile[file];
END;
END;
PerformImmediateFileActions:
PUBLIC
PROCEDURE [fileInstance: FileInstance.Handle] =
BEGIN
file: FileMap.Handle = FileInstance.GetFileHandle[fileInstance];
trans: TransactionMap.Handle = FileInstance.GetTransHandle[fileInstance];
description: LogMap.FileDescription = LogMap.DescribeFile[file, trans];
IF description.registered
AND ~description.deleted
THEN
BEGIN
LeaderPage.CarryOut[fileInstance: fileInstance];
IF description.sizeChanged
THEN
BEGIN
Size changed by this transaction. Note: compare desired size with actual size (rather than with committed size), since client may have extended file and later shortened it under the same transaction.
currentSize: PageCount = FilePageMgr.GetSize[file];
newSize: PageCount = LogMap.GetUncommittedSize[file: file, trans: trans];
Size increases are never deferred, so we should not encounter an intention to lengthen the file. (This is true during recovery as well.)
IF newSize>currentSize THEN ERROR;
IF newSize<currentSize THEN FilePageMgr.SetSize[fileHandle: file, size: newSize];
LogMap.SetCommittedSize[file: file, size: newSize];
LogMap.SetUncommittedSize[file: file, trans: NIL, size: LAST[PageCount]];
END;
At this point there should be nothing in the LogMap belonging to trans except possibly for deferred delete and page write intentions.
END;
END;
PerformDeferredFileActions:
PUBLIC
PROCEDURE [fileInstance: FileInstance.Handle] =
BEGIN
file: FileMap.Handle = FileInstance.GetFileHandle[fileInstance];
trans: TransactionMap.Handle = FileInstance.GetTransHandle[fileInstance];
description: LogMap.FileDescription = LogMap.DescribeFile[file, trans];
IF description.registered
THEN
BEGIN
IF description.deleted THEN FilePageMgr.Delete[file]
ELSE FilePrivate.PerformDeferredPageActions[fileInstance];
LogMap.UnregisterTrans[file, trans];
END;
END;
UndoFileActions:
PUBLIC
PROCEDURE [fileInstance: FileInstance.Handle] =
BEGIN
file: FileMap.Handle = FileInstance.GetFileHandle[fileInstance];
trans: TransactionMap.Handle = FileInstance.GetTransHandle[fileInstance];
description: LogMap.FileDescription = LogMap.DescribeFile[file, trans];
IF description.registered
THEN
BEGIN
IF description.created THEN FilePageMgr.Delete[file ! FilePageMgr.NoSuchFile => IF FilePrivate.recovering THEN CONTINUE]
ELSE
IF description.sizeChanged
THEN
BEGIN
If file was extended beyond its committed size then truncate it.
currentSize: PageCount = FilePageMgr.GetSize[file];
committedSize: PageCount = LogMap.GetCommittedSize[file: file];
SELECT committedSize
FROM
> currentSize =>
Size decreases are always deferred, so we should never encounter an undo request whose effect would be to lengthen the file (except perhaps during recovery, if the aborted transaction is followed by a committed one which shortened the file).
IF ~FilePrivate.recovering THEN ERROR;
< currentSize =>
FilePageMgr.SetSize[fileHandle: file, size: committedSize];
ENDCASE;
END;
LogMap.UnregisterTrans[file, trans];
END;
END;
END.
CHANGE LOG
Edited on July 25, 1984 10:05:07 am PDT, by Kupfer
Added SkiPatrolLog probes and some comments.
changes to: DIRECTORY, Open, Create, WriteProperties, SetSize, RecoverCreate, FileActionsImpl
Edited on August 6, 1984 3:15:20 pm PDT, by Kupfer
Remove the possible race condition in SkiPatrolLog probes by assigning the PROC to a temporary variable.
changes to: Open, Create, WriteProperties, SetSize, Create, DIRECTORY
Edited on February 28, 1985 1:14:42 pm PST, by Kupfer
Add code to Open to record the file's name after all the permissions and such have been verified.