% TimedRedisMutex Time-limited mutex in Redis 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 TimedRedisMutex < mps.sync.TimedMutex
    
    properties (Access = private)
        serviceConnection  % Opaque handle to persistence service
        lockUUID           % Unique lock value; only we can release lock.
        uuidVarName        % Name of Redis var. generating UUID.
    end
    
    properties (Constant,Hidden)
        uuidVarPrefix = "#MutexUUID";
        releaseScript = ...
            sprintf(['if redis.call("get",KEYS[1]) == ARGV[1]\n' ...
                     'then\n' ...
                     '    return redis.call("del", KEYS[1])\n' ...
                     'else\n' ...
                     '    return 0\n', ...
                     'end\n']);
    end
    
    methods
        function obj = TimedRedisMutex(mutexName, connection, options)
        %TIMEDREDISMUTEX Create an expiring mutex in a Redis server.
        %   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);
        
            obj.lockUUID = string.empty;
            obj.uuidVarName = strcat(...
                mps.sync.TimedRedisMutex.uuidVarPrefix, "_", ...
                mutexName);
            
             % Save connection handle
            obj.serviceConnection = connection;
            
            if isempty(obj.serviceConnection.ServiceHandle)
                connectToService(obj);
            end

        end
        
        function tf = own(obj)
            tf = activeLease(obj);
        end
        
        function tf = acquire(obj, timeout)        
            if ~isempty(obj.lockUUID)
                % We can't acquire the lease if we own it already.
                if activeLease(obj)
                    tf = false;
                    return;
                end
                obj.lockUUID = string.empty;
            end
            
            id = getUUID(obj);
            idStr = num2str(id);
            result = [];
            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
              
                cn = get(obj.serviceConnection.ServiceHandle);
                result = doRedisCmd(cn, "set", obj.MutexName, idStr, ...
                                    "NX", "EX", num2str(obj.Expiration));
                
                % If we have a result, exit the loop
                if ~isempty(result), break; end
                
                oldState = pause('on');
                pause(5/1000);  % Five MS
                pause(oldState);
                currentTime = datetime;
                duration = currentTime - startTime;
            end
               
            tf = ischar(result) && strcmp(result,"OK");
            if tf
                obj.lockUUID = idStr;
            end
        end
        
        function tf = release(obj)
            tf = false;
            
            % Only release if the connection is still valid and we've
            % acquired the lock. Otherwise we'll error. And there's no 
            % point in that here, since the lock is no longer useful if
            % the connection is down or we haven't acquired it.
            if ~isempty(obj.lockUUID) && ... 
                    isvalid(obj.serviceConnection) && ...
                    obj.serviceConnection.ServiceHandle.IsActive 
                cn = get(obj.serviceConnection.ServiceHandle);
                result = doRedisCmd(cn, "eval", ...
                           mps.sync.TimedRedisMutex.releaseScript, ...
                           num2str(1), obj.MutexName, obj.lockUUID);
                tf = result ~= 0;
            end
            obj.lockUUID = string.empty;
        end
        
        function delete(obj)
            release(obj);
        end
    end
    
    methods (Access = protected)
        
        function [tf,c] = validateOrReconnect(obj, redisConnection)
            c = [];
            if validateRedisConnection(redisConnection)
                tf = true;   % Connection good
                c = redisConnection;
            else
                if isvalid(obj) && isvalid(obj.serviceConnection)
                    c = connectToRedis(obj.serviceConnection.Host, ...
                        obj.serviceConnection.Port, ...
                        obj.serviceConnection.Key);
                end
                tf = false;  % Bad connection, replace with new one.
            end
        end

        function c = connectToService(obj)
            c = connectToRedis(obj.serviceConnection.Host, ...
                               obj.serviceConnection.Port, ...
                               obj.serviceConnection.Key);
            rh = mps.cache.ResourceHandle(c, @disconnectFromRedis, ...
                         message('MPS:MATLAB:Cache:NoServiceConnection', ...
                                   obj.serviceConnection.ConnectionName, ...
                                   obj.serviceConnection.ProviderName), ...
                               @(oh)validateOrReconnect(obj, oh));
            obj.serviceConnection.ServiceHandle = rh;
        end        
        
        function tf = activeLease(obj)
            cn = get(obj.serviceConnection.ServiceHandle);
            result = doRedisCmd(cn, "get", obj.MutexName);
            tf = ~isempty(result) && strcmp(result, obj.lockUUID);
        end
        
        function id = getUUID(obj)
            cn = get(obj.serviceConnection.ServiceHandle);
            id = doRedisCmd(cn, "incr", obj.uuidVarName);
        end
        
        function tf = resetUUID(obj)
            cn = get(obj.serviceConnection.ServiceHandle);
            result = doRedisCmd(cn, "set", obj.uuidVarName, "0", "XX");
            tf = strcmp(result,"OK");
        end
    end
end
