Manage Application State in Deployed Archives
This example shows how to manage persistent data in application archives deployed to MATLAB® Production Server™. It uses the MATLAB Production Server RESTful API for MATLAB Function Execution 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.
A testing workflow for testing the functionality of the application in a MATLAB desktop environment before deploying it to the server.
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
(MATLAB) 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
,
where $MPS_INSTALL
/client/matlab/examples/persistence/TravelingSalesman$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 Supported MATLAB Runtime Versions for MATLAB Production Server.
Step 1: Write MATLAB Code That Uses Persistence Functions
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 usingmps.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
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
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 nodename
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 vectordistances
.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
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
(MATLAB) 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
, andlistDestinationProxy
for thefindRoute
,addDestination
, andlistDestination
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
to a writable folder on your system, for example,
$MPS_INSTALL
/client/matlab/examples/persistence/TravelingSalesman/tmp/persistence_example
. Start the MATLAB desktop and set the current working directory to
/tmp/persistence_example
using the cd
(MATLAB) 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 lifecycle of a local
persistence service.
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. Themps.cache.connect
function requires connection names to create data caches. The MATLAB Production Server administrator sets connection names in the cache configuration filemps_cache_config
. For details, see Configure Server to Use Redis. By using the same connection names in MATLAB desktop sessions, you enable your code to move from development through testing to production without change.Start the persistence service using
start
.>> start(ctrl);
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 clear cities to visit. Click Compute Path to display a route that visits all the cities.
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
to a writeable folder on your system, for example,
$MPS_INSTALL
/client/matlab/examples/persistence/TravelingSalesman/tmp/persistence_example
. Start the MATLAB desktop and set the current working directory to
/tmp/persistence_example
using the MATLAB
cd
(MATLAB) 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.
Create a MATLAB Production Server instance
Create a server from the system command line using
mps-new
. For more information, see Create Server Instance Using Command Line. If you have not already set up your server environment, seemps-setup
for more information.Create a new server
server_1
located in the foldertmp
.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.
Create a persistence service connection
The deployable archive requires a persistence service connection named
ScratchPad
. Use the dashboard to create theScratchPad
connection or copy the filemps_cache_config
from the example directory to the config directory of your server instance. If you already have anmps_cache_config
file in your config directory, edit it to add theScratchPad
connection as specified in the examplemps_cache_config
.Create a deployable archive with the Production Server Compiler App and deploy it to the server
Open Production Server Compiler app
MATLAB toolstrip: On the Apps tab, under Application Deployment, click Production Server Compiler.
MATLAB command prompt: Enter
productionServerCompiler
.
In the Application Type menu, select Deployable Archive.
In the Exported Functions field, add
findRoute.m
,listDestinations.m
andaddDestination.m
.Under Archive information, rename the archive to
TravelingSalesman
.Under Additional files required for your archive to run, add
Distances.xlsx
.Click Package.
The generated deployable archive
TravelingSalesman.ctf
is located in thefor_redistribution
folder of the project. Copy theTravelingSalesman.ctf
file to theauto_deploy
folder of the server,/tmp/server_1/auto_deploy
in this example, for hosting.
Start the server instance
Start the server from the system command line using
mps-start
.Alternatively, use the dashboard to start the server.mps-start -C /tmp/server_1
Start the persistence service
Start the persistence service from the system command line using
mps-cache
.Alternatively, use the dashboard to start and attach the persistence service.mps-cache start -C /tmp/server_1 --connection ScratchPad
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 atserver_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 clear 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
mps.cache.Controller
(MATLAB Compiler SDK) | mps.cache.DataCache
(MATLAB Compiler SDK) | mps.sync.TimedMATFileMutex
(MATLAB Compiler SDK) | mps.sync.TimedRedisMutex
(MATLAB Compiler SDK) | mps.cache.control
(MATLAB Compiler SDK) | mps.cache.connect
(MATLAB Compiler SDK) | mps.sync.mutex
(MATLAB Compiler SDK)