Skip to content

Commit

Permalink
Removed endpoints and tests for app start/stop and events
Browse files Browse the repository at this point in the history
  • Loading branch information
zack-rma committed Feb 20, 2025
1 parent 285fb00 commit 2cc7193
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 440 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,12 @@

package org.opendcs.odcsapi.res;

import java.io.IOException;
import java.net.ConnectException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.annotation.security.RolesAllowed;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
Expand All @@ -41,28 +37,17 @@
import decodes.tsdb.CompAppInfo;
import decodes.tsdb.ConstraintException;
import decodes.tsdb.DbIoException;
import decodes.tsdb.LockBusyException;
import decodes.tsdb.NoSuchObjectException;
import decodes.tsdb.TsdbCompLock;
import ilex.cmdline.StdAppSettings;
import opendcs.dai.LoadingAppDAI;
import org.opendcs.odcsapi.appmon.ApiEventClient;
import org.opendcs.odcsapi.beans.ApiAppEvent;
import org.opendcs.odcsapi.beans.ApiAppRef;
import org.opendcs.odcsapi.beans.ApiAppStatus;
import org.opendcs.odcsapi.beans.ApiLoadingApp;
import org.opendcs.odcsapi.dao.DbException;
import org.opendcs.odcsapi.errorhandling.DatabaseItemNotFoundException;
import org.opendcs.odcsapi.errorhandling.MissingParameterException;
import org.opendcs.odcsapi.errorhandling.WebAppException;
import org.opendcs.odcsapi.lrgsclient.ClientConnectionCache;
import org.opendcs.odcsapi.util.ApiConstants;
import org.opendcs.odcsapi.util.ApiEnvExpander;
import org.opendcs.odcsapi.util.ApiPropertiesUtil;
import org.opendcs.odcsapi.util.ProcWaiterCallback;
import org.opendcs.odcsapi.util.ProcWaiterThread;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Resources for editing, monitoring, stopping, and starting processes.
Expand All @@ -71,7 +56,6 @@
public final class AppResources extends OpenDcsResource
{
private static final String NO_APP_FOUND = "No such app with ID: %s";
private static final Logger LOGGER = LoggerFactory.getLogger(AppResources.class);

@Context private HttpServletRequest request;
@Context private HttpHeaders httpHeaders;
Expand Down Expand Up @@ -265,155 +249,6 @@ static ApiAppStatus map(LoadingAppDAI dai, TsdbCompLock lock) throws DbIoExcepti
return ret;
}


@GET
@Path("appevents")
@Produces(MediaType.APPLICATION_JSON)
@RolesAllowed({ApiConstants.ODCS_API_ADMIN, ApiConstants.ODCS_API_USER})
public Response getAppEvents(@QueryParam("appid") Long appId)
throws WebAppException, DbException
{
if (appId == null)
{
throw new MissingParameterException("Missing required appid parameter.");
}

HttpSession session = request.getSession(true);
ClientConnectionCache clientConnectionCache = ClientConnectionCache.getInstance();
Optional<ApiEventClient> cli = clientConnectionCache.getApiEventClient(appId, session.getId());
ApiAppStatus appStat = null;
try(LoadingAppDAI dai = getLegacyDatabase().makeLoadingAppDAO())
{
ApiEventClient apiEventClient = null;
appStat = getAppStatus(dai, appId);
if (appStat == null)
{
cli.ifPresent(c -> clientConnectionCache.removeApiEventClient(c, session.getId()));
throw new DatabaseItemNotFoundException("appid " + appId
+ " is not running (no lock found).");
}
else if (appStat.getPid() == null)
{
cli.ifPresent(c -> clientConnectionCache.removeApiEventClient(c, session.getId()));
throw new DatabaseItemNotFoundException("appid " + appId
+ " (" + appStat.getAppName() + ") is not running.");
}
else if (System.currentTimeMillis() - appStat.getHeartbeat().getTime() > 20000L)
{
cli.ifPresent(c -> clientConnectionCache.removeApiEventClient(c, session.getId()));
throw new DatabaseItemNotFoundException("appid " + appId
+ " (" + appStat.getAppName() + ") is not running (stale heartbeat).");
}
else if (appStat.getPid() != null && (!cli.isPresent() || appStat.getPid() != cli.get().getPid()))
{
// This means that the app was stopped and restarted since we last checked for events.
// Close the old client and open a new one with the correct PID.
cli.ifPresent(c -> clientConnectionCache.removeApiEventClient(c, session.getId()));

Integer port = appStat.getEventPort();
if (port == null)
{
return Response.status(HttpServletResponse.SC_OK)
.entity(new ArrayList<ApiAppEvent>()).build();
}
apiEventClient = new ApiEventClient(appId, appStat.getHostname(), port, appStat.getAppName(), appStat.getPid());
apiEventClient.connect();
// NOTE: event client added to user token ONLY if connect succeeds.
clientConnectionCache.addApiEventClient(apiEventClient, session.getId());
}
if(apiEventClient == null)
{
throw new DatabaseItemNotFoundException("No API Event Client found or created");
}
return Response.status(HttpServletResponse.SC_OK)
.entity(apiEventClient.getNewEvents()).build();
}
catch(ConnectException ex)
{
throw new WebAppException(HttpServletResponse.SC_CONFLICT,
String.format("Cannot connect to %s.", appStat.getAppName()), ex);
// Connection failed, event client not added to user token
}
catch(IOException ex)
{
cli.ifPresent(c -> clientConnectionCache.removeApiEventClient(c, session.getId()));
throw new WebAppException(HttpServletResponse.SC_CONFLICT,
String.format("Event socket to %s closed by app", appStat.getAppId()), ex);
}
}

@POST
@Path("appstart")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@RolesAllowed({ApiConstants.ODCS_API_ADMIN, ApiConstants.ODCS_API_USER})
public Response postAppStart(@QueryParam("appid") Long appId)
throws WebAppException, DbException
{
if (appId == null)
{
throw new MissingParameterException("appId parameter required for this operation.");
}

try (LoadingAppDAI dai = getLegacyDatabase().makeLoadingAppDAO())
{
// Retrieve ApiLoadingApp and ApiAppStatus
ApiLoadingApp loadingApp = mapLoading(dai.getComputationApp(DbKey.createDbKey(appId)));
ApiAppStatus appStat = getAppStatus(dai, appId);



// Error if already running and heartbeat is current
if (appStat != null && appStat.getPid() != null && appStat.getHeartbeat() != null
&& (System.currentTimeMillis() - appStat.getHeartbeat().getTime() < 20000L))
throw new WebAppException(HttpServletResponse.SC_METHOD_NOT_ALLOWED,
"App id=" + appId + " (" + loadingApp.getAppName() + ") is already running.");

// Error if no "startCmd" property
String startCmd = ApiPropertiesUtil.getIgnoreCase(loadingApp.getProperties(), "startCmd");
if (startCmd == null)
throw new WebAppException(HttpServletResponse.SC_PRECONDITION_FAILED,
"App id=" + appId + " (" + loadingApp.getAppName() + ") has no 'startCmd' property.");


StdAppSettings settings = new StdAppSettings();

int pid;

if (appStat == null || appStat.getPid() == null)
{
pid = appId.intValue();
}
else
{
pid = appStat.getPid().intValue();
}

// Obtain a lock on the app with a processId chosen from the DB and standard hostname
dai.obtainCompProcLock(dai.getComputationApp(DbKey.createDbKey(loadingApp.getAppId())),
pid, settings.getHostName());

// ProcWaiterThread runBackground to execute command, use callback.
ProcWaiterCallback pwcb = (procName, obj, exitStatus) ->
{
ApiLoadingApp loadingApp1 = (ApiLoadingApp)obj;
LOGGER.info("App Termination: app {} was terminated with exit status {}",
loadingApp1.getAppName(), exitStatus);
};

ProcWaiterThread.runBackground(ApiEnvExpander.expand(startCmd), "App:" + loadingApp.getAppName(),
pwcb, loadingApp);

return Response.status(HttpServletResponse.SC_OK)
.entity("App with ID " + appId + " (" + loadingApp.getAppName() + ") started.").build();
}
catch (DbIoException | NoSuchObjectException | IOException | LockBusyException ex)
{
throw new WebAppException(HttpServletResponse.SC_BAD_REQUEST,
String.format("Error attempting to start appId=%s", appId), ex);
}
}

static ApiLoadingApp mapLoading(CompAppInfo app)
{
ApiLoadingApp ret = new ApiLoadingApp();
Expand All @@ -427,82 +262,4 @@ static ApiLoadingApp mapLoading(CompAppInfo app)
return ret;
}

@POST
@Path("appstop")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@RolesAllowed({ApiConstants.ODCS_API_ADMIN, ApiConstants.ODCS_API_USER})
public Response postAppStop(@QueryParam("appid") Long appId)
throws WebAppException, DbException
{
if (appId == null)
{
throw new MissingParameterException("appId parameter required for this operation.");
}

try (LoadingAppDAI dai = getLegacyDatabase().makeLoadingAppDAO())
{
// Retrieve ApiLoadingApp and ApiAppStatus
ApiLoadingApp loadingApp = mapLoading(dai.getComputationApp(DbKey.createDbKey(appId)));

ApiAppStatus appStat = getAppStatus(dai, appId);

if (appStat == null || appStat.getPid() == null)
{
throw new DatabaseItemNotFoundException(
"appId " + appId + "(" + loadingApp.getAppName() + ") not currently running.");
}

dai.releaseCompProcLock(new TsdbCompLock(DbKey.createDbKey(appId), appStat.getPid().intValue(),
appStat.getHostname(), appStat.getHeartbeat(), appStat.getStatus()));

return Response.status(HttpServletResponse.SC_OK)
.entity("App with ID " + appId + " (" + loadingApp.getAppName() + ") terminated.").build();
}
catch (NoSuchObjectException e)
{
throw new DatabaseItemNotFoundException(String.format("No such app found with ID %s", appId), e);
}
catch (DbIoException ex)
{
throw new WebAppException(HttpServletResponse.SC_BAD_REQUEST,
String.format("Error attempting to stop appId=%s", appId), ex);
}
}

private static ApiAppStatus getAppStatus(LoadingAppDAI dai, Long appId) throws DbException
{
try
{
List<TsdbCompLock> locks = dai.getAllCompProcLocks();
for (TsdbCompLock lock : locks)
{
if(lock.getAppId().getValue() == appId)
{
return map(dai, lock);
}
}

Optional<ApiAppStatus> appStatus = dai.listComputationApps(false)
.stream()
.map(AppResources::statusMap)
.filter(app -> app.getAppId().equals(appId))
.findFirst();
return appStatus.orElse(null);
}
catch (DbIoException ex)
{
throw new DbException(String.format("Error retrieving locks for app ID: %s", appId), ex);
}
}

static ApiAppStatus statusMap(CompAppInfo app)
{
ApiAppStatus ret = new ApiAppStatus();
ret.setAppId(app.getAppId().getValue());
ret.setAppName(app.getAppName());
ret.setAppType(app.getAppType());
return ret;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.opendcs.odcsapi.res.AppResources.map;
import static org.opendcs.odcsapi.res.AppResources.mapLoading;
import static org.opendcs.odcsapi.res.AppResources.statusMap;

final class AppResourcesTest
{
Expand Down Expand Up @@ -116,26 +115,4 @@ void testLoadingAppMap()
assertEquals(compAppInfo.getManualEditApp(), app.isManualEditingApp());
assertEquals(compAppInfo.getProperties(), app.getProperties());
}

@Test
void testAppStatusMap()
{
// Most of these fields are not mapped to the ApiAppStatus object
CompAppInfo compAppInfo = new CompAppInfo();
compAppInfo.setManualEditApp(true);
compAppInfo.setAppName("Computation application");
compAppInfo.setComment("Computation to find the volume of a river");
compAppInfo.setLastModified(Date.from(Instant.parse("2021-07-01T00:00:00Z")));
compAppInfo.setAppId(DbKey.createDbKey(151615L));
compAppInfo.setNumComputations(1);
Properties properties = new Properties();
properties.setProperty("compRef", "applicationValue");
compAppInfo.setProperties(properties);

ApiAppStatus appStatus = statusMap(compAppInfo);
assertNotNull(appStatus);
assertEquals(compAppInfo.getAppName(), appStatus.getAppName());
assertEquals(compAppInfo.getAppId().getValue(), appStatus.getAppId());
assertEquals(compAppInfo.getAppType(), appStatus.getAppType());
}
}
Loading

0 comments on commit 2cc7193

Please sign in to comment.