Example: Calculate the Shortest Route Between Cities Using Persistence

This example shows how to manage persistent data in application archives deployed to MATLAB® Production Server™. It uses the MATLAB Production Server RESTful API and JSON to connect one or more instances of a MATLAB app to an archive deployed on the server.

MATLAB Production Server workers are stateless. Persistence provides a mechanism to maintain state by caching data between multiple calls to MATLAB code deployed on the server. Multiple workers have access to the cached data.

The example describes two workflows.

  1. A testing workflow for testing the functionality of the application in a MATLAB desktop environment before deploying it to the server.

  2. A deployment workflow that uses an active MATLAB Production Server instance to deploy the archive.

To demonstrate how to use persistence, this example uses the traveling salesman problem, which involves finding the shortest possible route between cities. This implementation stores a persistent MATLAB graph object in the data cache. Cities form the nodes of the graph and the distances between the cities form the weights associated with the graph edges. In this example, the graph is a complete graph. The testing workflow uses the local version of the route-finding functions. The deployment workflow uses route-finding-functions that are packaged into an archive and deployed to the server. The MATLAB app calls the route-finding functions. These functions read from and write graph data to the cache.

The code for the example is located at $MPS_INSTALL/client/matlab/examples/persistence/TravelingSalesman, where $MPS_INSTALL is the location where MATLAB Production Server is installed.

To host a deployable archive created with the Production Server Compiler app, you must have a version of MATLAB Runtime installed that is compatible with the version of MATLAB you use to create your archive. For more information, see Download and Install the MATLAB Runtime.

Step 1: Write MATLAB Code that uses Persistence Functions

  1. Write a function to initialize persistent data

    Write a function to check whether a graph of cities and distances exists in the data cache. If the graph does not exist, create it from an Excel® spreadsheet that contains the distance data and write it to the cache. Because only one MATLAB Production Server worker at a time can perform this write operation, use a synchronization lock to ensure that data initialization happens only once.

    Connect to the cache that stores the distance data or create it if it does not exist using mps.cache.connect. Acquire a lock on a mutex using mps.sync.mutex for the duration of the write operation. Release the lock once the data is written to the cache.

    Initialize the distance data using the loadDistanceData function.

    function tf = loadDistanceData(connectionName, cacheName)
        c = mps.cache.connect(cacheName,'Connection',connectionName);    
        tries = 0;
    
        while isKey(c,'Distances') == false && tries < 6
            lk = mps.sync.mutex('DistanceData','Connection',connectionName);       
            if acquire(lk,10)
                if isKey(c,'Distances') == false
                    g = initDistanceData('Distances.xlsx');
                    c.Distances = g;
                end
                release(lk);
            end
            tries = tries + 1;
        end   
        tf = isKey(c,'Distances');
    end
  2. Write functions to read persistent data

    Write a function to read the distance data graph from the data cache. Because reading data from the cache is an idempotent operation, you do not need to use synchronization locks. Connect to the cache using mps.cache.connect and then retrieve the graph.

    Read the graph from the cache and convert it into a cell array using the listDestinations function.

    Calculate the shortest possible route using the findRoute function. Use the nearest neighbor algorithm, by starting at a given city and repeatedly visiting the next nearest city until all cities have been visited.

    function destinations = listDestinations()
        c = mps.cache.connect('TravelingSalesman','Connection','ScratchPad');   
        if loadDistanceData('ScratchPad','TravelingSalesman') == false
            error('Failed to load distance data. Cannot continue.');
        end
        
        g = c.Distances;
        destinations = table2array(g.Nodes);
    end
    function [route,distance] = findRoute(start,destinations)
        c = mps.cache.connect('TravelingSalesman','Connection','ScratchPad');    
        if loadDistanceData('ScratchPad','TravelingSalesman') == false
            error('Failed to load distance data. Cannot continue.');
        end
            
        g = c.Distances;    
        route = {start};
        distance = 0;
        current = start;
        
        while ~isempty(destinations)
            minDistance = Inf;
            nextSegment = {};        
            for n = 1:numel(destinations)
                [p,d] = shortestpath(g,current,destinations{n});
                if d < minDistance
                    nextSegment = p(2:end);
                    minDistance = d;
                end
            end
    
            current = nextSegment{end};
            distance = distance + minDistance;
            destinations = setdiff(destinations,current);        
            route = [ route nextSegment ];
        end
    end
  3. Write a function to modify persistent data

    Write a function to add a new city. Adding a city modifies the graph stored in the data cache. Because this operation requires writing to the cache, use the mps.sync.mutex function described in Step 1 for locking. After adding a city, check that the graph is still complete by confirming that the distance between every pair of cities is known.

    Add a city using the addDestination function. Adding a city adds a new graph node name along with new edges connecting this node to all existing nodes in the graph. The weights of the newly added edges are given by the vector distances. destinations is a cell array of character vectors that has the names of other cities in the graph.

    function count = addDestination(name, destinations, distances)  
        count = 0;
        c = mps.cache.connect('TravelingSalesman','Connection','ScratchPad');   
        if loadDistanceData('ScratchPad','TravelingSalesman') == false
            error('Failed to load distance data. Cannot continue.');
        end
        
        lk = mps.sync.mutex('DistanceData','Connection','ScratchPad');  
        if acquire(lk,10)
            g = c.Distances;      
            newDestinations = setdiff(g.Nodes.Name, destinations);       
            if ~isempty(newDestinations)
                error('MPS:Example:TSP:MissingDestinations', ...
                      'Add distances for missing destinations: %s', ...
                    strjoin(newDestinations,', '));
            end
            
            src = repmat({name},1,numel(destinations));
            g = addedge(g, src, destinations, distances);
            c.Distances = g;
            release(lk);
            count = numnodes(g);
        end   
    end
    

  4. Write a MATLAB app to call route-finding functions

    Write a MATLAB app that wraps the functions described in Steps 2 and 3 in their respective proxy functions. The app allows you to specify a host and a port. For testing, invoke the local version of the route-finding functions when the host is blank and the port has the value 0. For the deployment workflow, invoke the deployed functions on the server running on the specified host and port. Use the webwrite function to send HTTP POST requests to the server.

    For more information on how to write an app, see Create and Run a Simple App Using App Designer (MATLAB).

    Write the proxy functions findRouteProxy, addDestinationProxy, and listDestinationProxy for the findRoute, addDestination, and listDestination functions, respectively.

            function destinations = listDestinationsProxy(app)
                if isempty(app.HostEditField.Value) && ...
                        app.PortEditField.Value <= 0
                    destinations = listDestinations();
                    return;
                end
            
                listDestinations_OPTIONS = weboptions('MediaType','application/json','Timeout',60,'ContentType','raw');
                listDestinations_HOST = app.HostEditField.Value;
                listDestinations_PORT = app.PortEditField.Value;
                noInputJSON = '{ "rhs": [], "nargout": 1 }';
                destinations_JSON = ...
                webwrite(sprintf('http://%s:%d/TravelingSalesman/listDestinations',listDestinations_HOST,listDestinations_PORT), noInputJSON, listDestinations_OPTIONS);
                if iscolumn(destinations_JSON), destinations_JSON = destinations_JSON'; end
                destinations_RESPONSE = mps.json.decoderesponse(destinations_JSON);
                if isstruct(destinations_RESPONSE)
                    error(destinations_RESPONSE.id,destinations_RESPONSE.message);
                else
                    if nargout > 0, destinations = destinations_RESPONSE{1}; end
                end        
            end
            function [route,distance] = findRouteProxy(app,start,destinations)
                if isempty(app.HostEditField.Value) && ...
                        app.PortEditField.Value <= 0
                    [route,distance] = findRoute(start,destinations);
                    return;
                end  
                findRoute_OPTIONS = weboptions('MediaType','application/json','Timeout',60,'ContentType','raw');
                findRoute_HOST = app.HostEditField.Value;
                findRoute_PORT = app.PortEditField.Value;
                start_destinations_DATA = {};
                if nargin > 0, start_destinations_DATA = [ start_destinations_DATA { start } ]; end
                if nargin > 1, start_destinations_DATA = [ start_destinations_DATA { destinations } ]; end
                route_distance_JSON = ...
                    webwrite(sprintf('http://%s:%d/TravelingSalesman/findRoute',findRoute_HOST,findRoute_PORT), mps.json.encoderequest(start_destinations_DATA,'nargout',nargout), findRoute_OPTIONS);
                if iscolumn(route_distance_JSON), route_distance_JSON = route_distance_JSON'; end
                route_distance_RESPONSE = mps.json.decoderesponse(route_distance_JSON);
                if isstruct(route_distance_RESPONSE)
                    error(route_distance_RESPONSE.id,route_distance_RESPONSE.message);
                else
                    if nargout > 0, route = route_distance_RESPONSE{1}; end
                    if nargout > 1, distance = route_distance_RESPONSE{2}; end
                end
            end
            function count = addDestinationProxy(app, name, destinations,distances)
                if isempty(app.HostEditField.Value) && ...
                        app.PortEditField.Value <= 0
                    count = addDestination(name, destinations,distances);
                    return;
                end
                        
                addDestination_OPTIONS = weboptions('MediaType','application/json','Timeout',60,'ContentType','raw');
                addDestination_HOST = app.HostEditField.Value;
                addDestination_PORT = app.PortEditField.Value;
                name_destinations_distances_DATA = {};
                if nargin > 0, name_destinations_distances_DATA = [ name_destinations_distances_DATA { name } ]; end
                if nargin > 1, name_destinations_distances_DATA = [ name_destinations_distances_DATA { destinations } ]; end
                if nargin > 2, name_destinations_distances_DATA = [ name_destinations_distances_DATA { distances } ]; end
                count_JSON = ...
                    webwrite(sprintf('http://%s:%d/TravelingSalesman/addDestination',addDestination_HOST,addDestination_PORT), mps.json.encoderequest(name_destinations_distances_DATA,'nargout',nargout), addDestination_OPTIONS);
                if iscolumn(count_JSON), count_JSON = count_JSON'; end
                count_RESPONSE = mps.json.decoderesponse(count_JSON);
                if isstruct(count_RESPONSE)
                    error(count_RESPONSE.id,count_RESPONSE.message);
                else
                    if nargout > 0, count = count_RESPONSE{1}; end
                end
            end

Step 2: Run Example in Testing Workflow

Test the example code in the MATLAB desktop environment. To do so, copy the all the files located at $MPS_INSTALL/client/matlab/examples/persistence/TravelingSalesman to a writable folder on your system, for example, /tmp/persistence_example. Start the MATLAB desktop and set the current working directory to /tmp/persistence_example using the cd command.

For testing purposes, control a persistence service from the MATLAB desktop with the mps.cache.control function. This function returns an mps.cache.Controller object that manages the life cycle of a local persistence service.

  1. Create an mps.cache.Controller object for a local persistence service that uses the Redis™ persistence provider.

    >> ctrl = mps.cache.control('ScratchPad', 'Redis', 'Port', 8675);

    When active, this controller enables a connection named ScratchPad. Connection names link caches to storage locations in persistence services. The mps.cache.connect function requires connection names to create data caches. The MATLAB Production Server administrator sets connection names in the cache configuration file mps_cache_config. By using the same connection names in MATLAB desktop sessions, you enable your code to move from development through testing to production without change.

  2. Start the persistence service using start.

    >> start(ctrl);
  3. Start the TravelingSalesman route-finding app that uses the persistence service.

    >> TravelingSalesman

    The app starts with default values for Host and Port.

    Click Load Cities to load the list of cities. Use the Start menu to set a starting location and the >> and << buttons to select and deselect cities to visit. Click Compute Path to display a route that visits all the cities.

  4. When you close the app, stop the persistence service using stop. Stopping a persistence service will delete the data stored by that service.

    >> stop(ctrl);

Step 3: Run Example in Deployment Workflow

To run the example in the deployment workflow, copy the all the files located at $MPS_INSTALL/client/matlab/examples/persistence/TravelingSalesman to a writeable folder on your system, for example, /tmp/persistence_example. Start the MATLAB desktop and set the current working directory to /tmp/persistence_example using the MATLAB cd command.

The deployment workflow manages the lifetime of a persistence service outside of a MATLAB desktop environment and invokes the route-finding functions packaged in an archive deployed to the server.

  1. Create a MATLAB Production Server instance

    Create a server from the system command line using mps-new. For more information, see Create a Server. If you have not already set up your server environment, see mps-setup for more information.

    Create a new server server_1 located in the folder tmp.

    mps-new /tmp/server_1

    Alternatively, use the MATLAB Production Server dashboard to create a server. For more information, see Set Up and Log In to MATLAB Production Server Dashboard.

  2. Create a persistence service connection

    The deployable archive requires a persistence service connection named ScratchPad. Use the dashboard to create the ScratchPad connection or copy the file mps_cache_config from the example directory to the config directory of your server instance. If you already have an mps_cache_config file in your config directory, edit it to add the ScratchPad connection as specified in the example mps_cache_config.

  3. Create a deployable archive with the Production Server Compiler App and deploy it to the server

    1. Open Production Server Compiler app

      • MATLAB toolstrip: On the Apps tab, under Application Deployment, click Production Server Compiler.

      • MATLAB command prompt: Enter productionServerCompiler.

    2. In the Application Type menu, select Deployable Archive.

    3. In the Exported Functions field, add findRoute.m, listDestinations.m and addDestination.m.

    4. Under Archive information, rename the archive to TravelingSalesman.

    5. Under Additional files required for your archive to run, add Distances.xlsx.

    6. Click Package.

    7. The generated deployable archive TravelingSalesman.ctf is located in the for_redistribution folder of the project. Copy the TravelingSalesman.ctf file to the auto_deploy folder of the server, /tmp/server_1/auto_deploy in this example, for hosting.

  4. Start the server instance

    Start the server from the system command line using mps-start.

    mps-start -C /tmp/server_1
    Alternatively, use the dashboard to start the server.

  5. Start the persistence service

    Start the persistence service from the system command line using mps-cache.

    mps-cache start -C /tmp/server_1 --connection ScratchPad
    Alternatively, use the dashboard to start and attach the persistence service.

  6. Test the app

    Start the TravelingSalesman route-finding app that uses the persistence service.

    >> TravelingSalesman

    The app starts with empty values for Host and Port. Refer to the server configuration file main_config located at server_name/config to get the host and port values for your MATLAB Production Server instance. For this example, find the config file at /tmp/server_1/config. Enter the host and port values in the app.

    Click Load Cities to load the list of cities. Use the Start menu to set a starting location and the >> and << buttons to select and deselect cities to visit. Click Compute Path to display a route that visits all the cities.

The results from the testing environment workflow and the deployment environment workflow are the same.

See Also

| | | | | |

Related Topics