/*
Copyright 2013 Philipp Leitner
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package at.ac.tuwien.infosys.jcloudscale.vm.localVm;
import java.io.File;
import java.io.IOException;
import java.lang.ProcessBuilder.Redirect;
import java.lang.ref.ReferenceQueue;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger;
import javax.jms.JMSException;
import javax.naming.NamingException;
import at.ac.tuwien.infosys.jcloudscale.InvocationInfo;
import at.ac.tuwien.infosys.jcloudscale.configuration.JCloudScaleConfiguration;
import at.ac.tuwien.infosys.jcloudscale.exception.JCloudScaleException;
import at.ac.tuwien.infosys.jcloudscale.exception.ScalingException;
import at.ac.tuwien.infosys.jcloudscale.logging.Logged;
import at.ac.tuwien.infosys.jcloudscale.management.JCloudScaleReferenceManager;
import at.ac.tuwien.infosys.jcloudscale.messaging.TimeoutException;
import at.ac.tuwien.infosys.jcloudscale.messaging.objects.monitoring.CPUEvent;
import at.ac.tuwien.infosys.jcloudscale.messaging.objects.monitoring.RAMEvent;
import at.ac.tuwien.infosys.jcloudscale.migration.IMigrationEnabledJCloudScaleHost;
import at.ac.tuwien.infosys.jcloudscale.migration.MigrationEnabledVirtualHostProxy;
import at.ac.tuwien.infosys.jcloudscale.monitoring.CPUUsage;
import at.ac.tuwien.infosys.jcloudscale.monitoring.EventCorrelationEngine;
import at.ac.tuwien.infosys.jcloudscale.monitoring.IMetricsDatabase;
import at.ac.tuwien.infosys.jcloudscale.monitoring.MonitoringMetric;
import at.ac.tuwien.infosys.jcloudscale.monitoring.RAMUsage;
import at.ac.tuwien.infosys.jcloudscale.utility.CgLibUtil;
import at.ac.tuwien.infosys.jcloudscale.utility.ReflectionUtil;
import at.ac.tuwien.infosys.jcloudscale.utility.SerializationUtil;
import at.ac.tuwien.infosys.jcloudscale.vm.ClientCloudObject;
import at.ac.tuwien.infosys.jcloudscale.vm.CloudObjectState;
import at.ac.tuwien.infosys.jcloudscale.vm.CloudPlatformConfiguration;
import at.ac.tuwien.infosys.jcloudscale.vm.JCloudScaleClient;
import at.ac.tuwien.infosys.jcloudscale.vm.IHostPool;
import at.ac.tuwien.infosys.jcloudscale.vm.IdManager;
import at.ac.tuwien.infosys.jcloudscale.vm.VirtualHost;
import at.ac.tuwien.infosys.jcloudscale.vm.VirtualHostProxy;
@Logged
public class LocalVM extends VirtualHost
{
private static final String SERVER_MODE_COMMAND = "-server";
private static final String HEAP_SIZE_COMMAND = "-Xmx";
private static final String CLASSPATH_COMMAND = "-cp";
private static final String STANDARD_INSTANCE_SIZE_FLAG = "local.default";
protected CloudPlatformConfiguration config;
protected IMigrationEnabledJCloudScaleHost server;
protected String serverIp;
protected Process jvmProcess;
protected boolean startPerformanceMonitoring;
protected List<ClientCloudObject> cloudObjects = new CopyOnWriteArrayList<>();
protected Logger log;
protected IdManager idManager;
protected LocalVM(IdManager idManager, boolean startPerformanceMonitoring) {
this.idManager = idManager;
this.startPerformanceMonitoring = startPerformanceMonitoring;
this.log = JCloudScaleConfiguration.getLogger(this);
this.instanceSize = STANDARD_INSTANCE_SIZE_FLAG;
}
public LocalVM(LocalCloudPlatformConfiguration config, IdManager idManager, boolean startPerformanceMonitoring)
{
this(idManager, startPerformanceMonitoring);
this.config = config;
}
@Override
public UUID deployCloudObject(ClientCloudObject cloudObject, Object[] args, Class<?>[] paramTypes) {
try
{
// we add object here to have it as soon as possible
cloudObjects.add(cloudObject);
ensureHostStarted();
byte[] byteArgs = SerializationUtil.serializeToByteArray(args);
String[] paramNames = ReflectionUtil.getNamesFromClasses(paramTypes);
String id = server.createNewCloudObject(cloudObject.getCloudObjectClass().getName(), byteArgs, paramNames);
UUID objectId = UUID.fromString(id);
cloudObject.setId(objectId);
addManagedObject(cloudObject);
return objectId;
} catch(IOException e) {
throw new JCloudScaleException(e, "Could not serialize invocation parameters: " + Arrays.toString(args));
}
finally
{
lastRequestTime = new Date(System.currentTimeMillis());
}
}
@Override
public Object invokeCloudObject(UUID cloudObjectId, Method method, Object[] args, Class<?>[] paramTypes)
{
try
{
ensureHostStarted();
ClientCloudObject clientCo = managedObjects.get(cloudObjectId);
if(clientCo == null)
throw new JCloudScaleException("The object with id "+cloudObjectId+ " was not found or was not deployed yet.");
clientCo.addExecutingMethod(method.getName());
Object proxy = clientCo.getProxy();
byte[] retBytes = scheduleInvocation(cloudObjectId, method.getName(), args, paramTypes, proxy);
clientCo.removeExecutingMethod(method.getName());
Object returned = SerializationUtil.getObjectFromBytes(retBytes, this.getClass().getClassLoader());
// if this is a reference, replace it with a proxy
returned = CgLibUtil.replaceRefWithProxy(returned, this.getClass().getClassLoader());
return returned;
} catch(IOException e) {
e.printStackTrace();
throw new JCloudScaleException(e, "Could not serialize invocation parameters: " + Arrays.toString(args));
} catch (ClassNotFoundException e) {
e.printStackTrace();
throw new JCloudScaleException(e, "Could not deserialize return value. Class not found.");
} catch (InterruptedException e) {
e.printStackTrace();
throw new JCloudScaleException(e, "Interrupted while waiting for server to become free.");
} catch(JCloudScaleException e) {
// this should happen if the by-ref type did not have a default constructor
e.printStackTrace();
throw e;
} catch (Throwable e) {
e.printStackTrace();
throw new JCloudScaleException(e);
}
finally
{
lastRequestTime = new Date(System.currentTimeMillis());
}
}
@Override
public Object getCloudObjectFieldValue(UUID id, Field field) {
try
{
ensureHostStarted();
byte[] ser = server.getCloudObjectField(id.toString(), field.getName());
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Object retField = SerializationUtil.getObjectFromBytes(ser, classLoader);
return CgLibUtil.replaceRefWithProxy(retField, classLoader);
} catch(JCloudScaleException e) {
// this should happen if the by-ref type did not have a default constructor
e.printStackTrace();
throw e;
} catch (Throwable e) {
e.printStackTrace();
throw new JCloudScaleException(e, "Could not deserialize return value from field access");
}
finally
{
lastRequestTime = new Date(System.currentTimeMillis());
}
}
@Override
public void setCloudObjectFieldValue(UUID id, Field field, Object val) {
try
{
ensureHostStarted();
val = JCloudScaleReferenceManager.getInstance().processField(field, val);
byte[] ser = SerializationUtil.serializeToByteArray(val);
server.setCloudObjectField(id.toString(), field.getName(), ser);
} catch (IOException e) {
e.printStackTrace();
throw new JCloudScaleException(e, "Could not serialize value to set to field");
}
finally
{
lastRequestTime = new Date(System.currentTimeMillis());
}
}
@Override
public void destroyCloudObject(UUID cloudObjectId)
{
ensureHostStarted();
server.destroyCloudObject(cloudObjectId.toString());
ClientCloudObject cloudObject = managedObjects.get(cloudObjectId);
// we should be setting the state here anyway, as a client
// may still hold a reference to this cco
cloudObject.setDestructed();
cloudObjects.remove(cloudObject);
removedManagedObject(cloudObjectId);
lastRequestTime = new Date(System.currentTimeMillis());
}
@Override
public void refreshCloudObjects()
{
if(!isOnline())
return; // if the host is not online now, there's no point to send isAlive messages.
for(ClientCloudObject cco : cloudObjects)
{
log.info("Sending IsAlive message for object "+cco.getId()+" to host "+ id + "("+this.serverIp+")");
if(cco.getId() != null)
server.keepAliveCloudObject(cco.getId());
}
}
@Override
public void startupHost(IHostPool hostPool, String size) throws ScalingException
{
// see if we have a free ID available to us (for instance a static host)
// TODO: in principle, we should associate IDs with instance sizes, so that
// we know whether a given static instance is of the size we wanted
// however, for now, I just assume that if a user explicitly requests
// a given size, we are starting up dynamically and ignore static instance
UUID serverId = idManager.getFreeId(size == null);
if(serverId == null) {
launchHost(size);
// now, at some point, we should get a free ID, which we assign to this host
serverId = idManager.waitForId();
}
// check if this is an ID of a static or dynamic host
if(idManager.isStaticId(serverId))
isStatic = true;
this.id = serverId;
this.serverIp = idManager.getIpToId(serverId);
log.info("Server "+serverId+" has IP address "+serverIp);
// //sending configuration.
try
{
JCloudScaleConfiguration.getConfiguration().sendConfigurationToStaticHost(id);
log.info("Successfully sent configuration to host "+id+" ("+this.serverIp+")");
} catch (NamingException | JMSException | TimeoutException | IOException e)
{
throw new JCloudScaleException(e, "Failed to send configuration to host "+serverId + "("+this.serverIp+")");
}
server = new MigrationEnabledVirtualHostProxy(this.id);
JCloudScaleClient.getClient().addProxy(this.id, (VirtualHostProxy) server);
log.info("Created proxy for server");
if(startPerformanceMonitoring) {
registerCPUMetric();
registerRAMMetric();
log.info("Registered basic monitoring metrics for server");
}
startupTime = new Date(System.currentTimeMillis());
hostStarted();
this.scaleDownTask = new ScaleDownTask(hostPool);
log.info("Started scale-down task for new server");
}
@Override
public void close()
{
if(scaleDownTask != null)
{
scaleDownTask.close();
scaleDownTask = null;
}
hostShutdown();
if(startPerformanceMonitoring) {
EventCorrelationEngine.getInstance().unregisterMetric("CPULoad_"+id.toString());
EventCorrelationEngine.getInstance().unregisterMetric("RAMUsage_"+id.toString());
}
idManager.releaseId(id);
if(!isStaticHost()) {
server.shutdown();
idManager.removeId(id);
}
((VirtualHostProxy)server).close();
}
protected void launchHost(String size) {
this.instanceSize = size;
if(!(this.config instanceof LocalCloudPlatformConfiguration))
throw new JCloudScaleException("Failed to launch local virtual VM: preconfigured configuration is of the wrong type:"
+(this.config==null ?"NULL": this.config.getClass().getName())+"instead of LocalCloudPlatformConfiguration");
LocalCloudPlatformConfiguration config = (LocalCloudPlatformConfiguration)this.config;
File workingDir = new File(config.getStartupDirectory());
try
{
//For even higher platform independency, we can use ant to start new jvm.
String javaPath = config.getJavaPath();
ProcessBuilder pb = new ProcessBuilder(
/*JAVA executable*/ javaPath,
/*Class path of the server*/ CLASSPATH_COMMAND, config.getClasspath(), //should go as separate args.
/*Server startup class*/ config.getServerStartupClass()
);
// adding memory limit parameter
if(config.getJavaHeapSizeMB() > 0)
pb.command().add(1, HEAP_SIZE_COMMAND + config.getJavaHeapSizeMB()+"m");
// adding server mode parameter
if(config.isServerMode())
pb.command().add(1, SERVER_MODE_COMMAND);
// add custom JVM parameters as defined by the user
for(String arg : config.getCustomJVMArgs()) {
pb.command().add(1, arg);
}
pb.directory(workingDir);
// redirecting output to the same destination as of our process
//(so that server will write output to client's console/error stream.)
pb.redirectOutput(Redirect.INHERIT);
pb.redirectError(Redirect.INHERIT);
jvmProcess = pb.start();
try
{
Thread.sleep(400);
int exitCode = jvmProcess.exitValue();
String message = "JVM failed to launch. Exit code "+exitCode;
throw new ScalingException(message);
}
catch(IllegalThreadStateException | InterruptedException e1)
{
// ignore
}
}
catch (IOException e)
{
throw new ScalingException("Could not start local VM. Error message was: "+e.getMessage());
}
}
private byte[] scheduleInvocation(UUID cloudObjectId, String method, Object[] args, Class<?>[] paramTypes, Object obj)
throws InterruptedException, IOException {
byte[] byteArgs = SerializationUtil.serializeToByteArray(args);
String[] paramNames = ReflectionUtil.getNamesFromClasses(paramTypes);
String cloudObjectIdString = cloudObjectId.toString();
String invocationIdString = server.startInvokingCloudObject(cloudObjectIdString, method, byteArgs, paramNames);
UUID invocationId = UUID.fromString(invocationIdString);
addInvocation(cloudObjectId, invocationId);
ReflectionUtil.addInvocationInfo(obj,
new InvocationInfo(cloudObjectIdString, invocationIdString, method, args)
);
byte[] ret = ((VirtualHostProxy) server).waitForResult(invocationIdString);
ReflectionUtil.removeInvocationInfo(obj, invocationIdString);
removeInvocation(cloudObjectId, invocationId);
return ret;
}
@Override
public String getIpAddress() {
return serverIp;
}
@Override
public Class<?> getCloudObjectType(UUID cloudObjectId) throws JCloudScaleException {
ClientCloudObject clientCo = managedObjects.get(cloudObjectId);
Class<?> clazz = clientCo.getCloudObjectClass();
return clazz;
}
@Override
public Iterable<ClientCloudObject> getCloudObjects() {
return Collections.unmodifiableCollection(cloudObjects);
}
@Override
public int getCloudObjectsCount()
{
return cloudObjects.size();
}
@Override
public void suspendRunningInvocation(UUID cloudObject, UUID invocation) {
server.suspendInvocation(cloudObject.toString(), invocation.toString());
}
@Override
public void continueRunningInvocation(UUID cloudObject, UUID invocation) {
server.resumeInvocation(cloudObject.toString(), invocation.toString());
}
@Override
public CPUUsage getCurrentCPULoad() {
EventCorrelationEngine engine = EventCorrelationEngine.getInstance();
if(engine == null)
return null;
IMetricsDatabase db = engine.getMetricsDatabase();
Object val = db.getLastValue("CPULoad_"+id.toString());
if(val == null)
return null;
CPUEvent cpuEvent = (CPUEvent) val;
return
new CPUUsage(
cpuEvent.getSystemCpuLoad(),
cpuEvent.getNrCPUs(),
cpuEvent.getArch()
);
}
@Override
public RAMUsage getCurrentRAMUsage() {
EventCorrelationEngine engine = EventCorrelationEngine.getInstance();
if(engine == null)
return null;
IMetricsDatabase db = engine.getMetricsDatabase();
Object val = db.getLastValue("RAMUsage_"+id.toString());
if(val == null)
return null;
RAMEvent ramEvent = (RAMEvent) val;
return
new RAMUsage(
ramEvent.getMaxMemory(),
ramEvent.getUsedMemory(),
ramEvent.getFreeMemory()
);
}
@Override
public CloudObjectState getCloudObjectState(UUID cloudObject) {
if(!managedObjects.containsKey(cloudObject))
throw new JCloudScaleException("Cannot get state for unmanaged cloud object "+cloudObject.toString());
return managedObjects.get(cloudObject).getState();
}
@Override
public List<String> getExecutingMethods(UUID cloudObject) {
if(!managedObjects.containsKey(cloudObject))
throw new JCloudScaleException("Cannot get executing methods for unmanaged cloud object "+cloudObject.toString());
return managedObjects.get(cloudObject).getExecutingMethods();
}
// MIGRATION METHODS
@Override
public byte[] serializeToMigrate(UUID cloudObjectId) throws JCloudScaleException {
try {
ensureHostStarted();
return server.serializeToMigrate(cloudObjectId.toString());
} finally {
lastRequestTime = new Date(System.currentTimeMillis());
}
}
@Override
public void deployMigratedCloudObject(UUID cloudObjectId, Class<?> cloudObjectType,
byte[] serializedCloudObject, Object proxy, ReferenceQueue<Object> queue)
throws JCloudScaleException {
try {
ensureHostStarted();
server.deployMigratedCloudObject(cloudObjectId.toString(),
cloudObjectType.getName(), serializedCloudObject);
ClientCloudObject clientCo = new ClientCloudObject(cloudObjectId, cloudObjectType, proxy, queue);
cloudObjects.add(clientCo);
addManagedObject(clientCo);
} finally {
lastRequestTime = new Date(System.currentTimeMillis());
}
}
@Override
public void removeCloudObject(UUID cloudObjectId) throws JCloudScaleException {
try {
ensureHostStarted();
server.removeCloudObject(cloudObjectId.toString());
ClientCloudObject cloudObject = managedObjects.get(cloudObjectId);
cloudObjects.remove(cloudObject);
removedManagedObject(cloudObjectId);
} finally {
lastRequestTime = new Date(System.currentTimeMillis());
}
}
@Override
public long getCloudObjectSize(UUID cloudObjectId) {
throw new RuntimeException("Not implemented!");
}
@Override
public Object getProxyObject(UUID cloudObjectId) {
try {
ensureHostStarted();
if (!managedObjects.containsKey(cloudObjectId))
throw new JCloudScaleException("Cannot get proxy for unmanaged cloud object " + cloudObjectId);
return managedObjects.get(cloudObjectId).getProxy();
} finally {
lastRequestTime = new Date(System.currentTimeMillis());
}
}
private void registerCPUMetric() {
MonitoringMetric cpuMetric = new MonitoringMetric();
cpuMetric.setName("CPULoad_"+id.toString());
cpuMetric.setEpl("select * from CPUEvent");
// cpuMetric.setResultField("cpuLoad");
EventCorrelationEngine.getInstance().registerMetric(cpuMetric);
}
private void registerRAMMetric() {
MonitoringMetric ramMetric = new MonitoringMetric();
ramMetric.setName("RAMUsage_"+id.toString());
ramMetric.setEpl("select * from RAMEvent");
// ramMetric.setResultField("usedMemory");
EventCorrelationEngine.getInstance().registerMetric(ramMetric);
}
}