% TimedMATFileMutex Time-limited mutex in MatlabTest persistence service.
%
% MATLAB Production Server workers may use mutex objects to cooperatively
% coordinate access to shared data. 
%
% See Also mutex, mps.cache.connect, ServiceMutex

% Copyright 2020-2021 The MathWorks, Inc.
classdef TimedMATFileMutex < mps.sync.TimedMutex
    
    properties (Access = private)
        serviceHandle    % A MATFileService object
        lockPath         % If this dir. exists, we hold the lock.
        lockFolder       % Folder in MATFileService that contains locks.
        expireFile       % Full path to file holding expiration time.
    end
    
    properties (Constant,Hidden)
        lockDir = "locks";
        expireFileName = "expirationDate";
        secondsPerDay = 60 * 60 * 24;
    end
        
    methods
        function obj = TimedMATFileMutex(mutexName, connection, options)
        %TIMEDMATFILEMUTEX Create an expiring mutex in a MatlabTest service.
        %   Do not call this function directly. Call mps.sync.mutex to
        %   create a mutex.
        %
        %   See Also mutex, ServiceMutex, mps.cache.connect            
            obj = obj@mps.sync.TimedMutex(connection.ConnectionName, ...
                                          mutexName, options.Expiration);
                                      
            if isempty(connection.ServiceHandle)       
                ap = mps.persistence.internal.ActiveServices.getInstance();
                mfs = get(ap,connection.Folder);
                if isempty(mfs)
                    error(message('MPS:MATLAB:Cache:InactiveService',...
                        connection.ProviderName, connection.ConnectionName));
                end
                connection.ServiceHandle = mfs;
                obj.serviceHandle = mfs;
            else
                obj.serviceHandle = connection.ServiceHandle;
            end
            obj.lockFolder = strcat(connection.Folder,filesep,obj.lockDir);
            obj.lockPath = string.empty;
        end
        
        function tf = own(obj)
            tf = activeLease(obj);
        end
        
        function tf = acquire(obj, timeout)
        % acquire Acquire a lock. Keep trying for timeout seconds.
            if ~isempty(obj.lockPath)
               % We can't acquire the lease if we own it already.
                if activeLease(obj)
                    tf = false;
                    return;
                end
                % If the lockPath was not empty (meaning we thought we
                % had the lock) but our lease is no longer active, we 
                % don't have the lock.
                obj.lockPath = string.empty;
            end
            
            obj.lockPath = getUniquePath(obj);
            locked = false;
            if nargin < 2
                timeout = 0;
            end
            startTime = datetime;
            currentTime = startTime;
            duration = currentTime - startTime;
            % Until we've timed out, keep trying to acquire the lock.
            while seconds(duration) <= timeout
              
                locked = mkdir(obj.lockPath);
                
                % If we acquired the lock (created the directory)
                % exit the loop.
                if locked, break; end
                
                oldState = pause('on');
                pause(5/1000);  % Five MS
                pause(oldState);
                currentTime = datetime;
                duration = currentTime - startTime;
            end
               
            expire(obj, obj.Expiration);
            
            if ~locked
                obj.lockPath = string.empty;
            end
            tf = locked;
        end
        
        function tf = release(obj)
        % release Release a lock if we acquired it.
           
            ok = ~isempty(obj.lockPath) && rmdir(obj.lockPath, 's'); 
            tf = ok == 1;
            
            obj.lockPath = string.empty;
        end
        
        function delete(obj)
            release(obj);
        end
    end
    
    methods (Access = protected)
        
        function expire(obj, when)
            obj.expireFile = strcat(obj.lockPath,filesep, ...
                                    obj.expireFileName);
            fp = fopen(obj.expireFile,'wb');
            if fp < 0
                error(message('MPS:MATLAB:Sync:UnwriteableExpiration', ...
                    obj.MutexName));
            else
                t = now + (when / obj.secondsPerDay);
                fwrite(fp,t,'double');
                fclose(fp);
            end
        end
        
        function t = expirationTime(obj)
            fp = fopen(obj.expireFile, 'rb');
            if fp < 0
                error(message('MPS:MATLAB:Sync:UnreadableExpiration', ...
                    obj.MutexName));
            else
                t = fread(fp,1,'double');
                fclose(fp);
            end
        end
        
        function tf = activeLease(obj)
        % activeLease Do we own the lease? And has it not yet expired?
            tf = false;
            if ~isempty(obj.expireFile) && exist(obj.expireFile, 'file')
                expireTime = expirationTime(obj);
                n = now;
                if n < expireTime
                    tf = true;
                else
                    % Lease has expired. Remove the locking machinery.
                    release(obj);
                end
            end
        end
                
        
        function pth = getUniquePath(obj)
            pth = tempname(obj.lockFolder);
        end
        
    end
end