package org.cloudname.service;
import org.cloudname.core.CloudnameBackend;
import org.cloudname.core.CloudnamePath;
import org.cloudname.core.LeaseHandle;
import org.cloudname.core.LeaseListener;
import org.cloudname.core.LeaseType;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Service discovery implementation. Use registerService() and addServiceListener() to register
* and locate services.
*
* @author stalehd@gmail.com
*/
public class CloudnameService implements AutoCloseable {
private static final Logger LOG = Logger.getLogger(CloudnameService.class.getName());
private final CloudnameBackend backend;
private final List<ServiceHandle> handles = new ArrayList<>();
private final List<LeaseListener> temporaryListeners = new ArrayList<>();
private final List<LeaseListener> permanentListeners = new ArrayList<>();
private final Set<ServiceCoordinate> permanentUpdatesInProgress = new CopyOnWriteArraySet<>();
private final Object syncObject = new Object();
private final Random random = new Random();
private static final int MAX_COORDINATE_RETRIES = 10;
/**
* Create the service interface.
*
* @param backend backend implementation to use
* @throws IllegalArgumentException if parameter is invalid
*/
public CloudnameService(final CloudnameBackend backend) {
if (backend == null) {
throw new IllegalArgumentException("Backend can not be null");
}
this.backend = backend;
}
/**
* Register an instance with the given service coordinate. The service will get its own
* instance coordinate under the given service coordinate.
*
* @param serviceCoordinate The service coordinate that the service (instance) will attach to
* @param serviceData Service data for the instance
* @return ServiceHandle a handle the client can use to manage the endpoints for the service.
* The most typical use case is to register all endpoints
* @throws IllegalArgumentException if the parameters are invalid
*/
public ServiceHandle registerService(
final ServiceCoordinate serviceCoordinate, final ServiceData serviceData) {
if (serviceCoordinate == null) {
throw new IllegalArgumentException("Coordinate cannot be null");
}
if (serviceData == null) {
throw new IllegalArgumentException("Service Data cannot be null");
}
// Create unique coordinate; the coordinate is just a random number.
int numRetries = 0;
LeaseHandle leaseHandle = null;
while (numRetries < MAX_COORDINATE_RETRIES && leaseHandle == null) {
final CloudnamePath newCoordinate = new CloudnamePath(
serviceCoordinate.toCloudnamePath(), Long.toHexString(random.nextLong()));
leaseHandle = backend.createLease(LeaseType.TEMPORARY,
newCoordinate, serviceData.toJsonString());
numRetries++;
}
if (numRetries == MAX_COORDINATE_RETRIES && leaseHandle == null) {
LOG.severe("Could not find available coordinate after " + MAX_COORDINATE_RETRIES
+ " for service " + serviceCoordinate);
return null;
}
final ServiceHandle serviceHandle = new ServiceHandle(
new InstanceCoordinate(leaseHandle.getLeasePath()), serviceData, leaseHandle);
synchronized (syncObject) {
handles.add(serviceHandle);
}
return serviceHandle;
}
/**
* Add listener for service events. This only applies to ordinary services.
*
* @param coordinate The coordinate to monitor.
* @param listener Listener getting notifications on changes.
* @throws IllegalArgumentException if parameters are invalid
*/
public void addServiceListener(
final ServiceCoordinate coordinate, final ServiceListener listener) {
if (coordinate == null) {
throw new IllegalArgumentException("Coordinate can not be null");
}
if (listener == null) {
throw new IllegalArgumentException("Listener can not be null");
}
// Just create the corresponding listener on the backend and translate the parameters
// from the listener.
final LeaseListener leaseListener = new LeaseListener() {
@Override
public void leaseCreated(final CloudnamePath path, final String data) {
final InstanceCoordinate instanceCoordinate = new InstanceCoordinate(path);
final ServiceData serviceData = ServiceData.fromJsonString(data);
listener.onServiceCreated(instanceCoordinate, serviceData);
}
@Override
public void leaseRemoved(final CloudnamePath path) {
final InstanceCoordinate instanceCoordinate = new InstanceCoordinate(path);
listener.onServiceRemoved(instanceCoordinate);
}
@Override
public void dataChanged(final CloudnamePath path, final String data) {
final InstanceCoordinate instanceCoordinate = new InstanceCoordinate(path);
final ServiceData serviceData = ServiceData.fromJsonString(data);
listener.onServiceDataChanged(instanceCoordinate, serviceData);
}
};
synchronized (syncObject) {
temporaryListeners.add(leaseListener);
}
backend.addLeaseCollectionListener(coordinate.toCloudnamePath(), leaseListener);
}
/**
* Create a permanent service. The service registration will be kept when the client exits. The
* service will have a single endpoint.
*
* @param coordinate The service's coordinate
* @param endpoint Endpoint for service * @return true if service is created
*/
public boolean createPermanentService(
final ServiceCoordinate coordinate, final Endpoint endpoint) {
if (coordinate == null) {
throw new IllegalArgumentException("Service coordinate can't be null");
}
if (endpoint == null) {
throw new IllegalArgumentException("Endpoint can't be null");
}
return (backend.createLease(
LeaseType.PERMANENT, coordinate.toCloudnamePath(), endpoint.toJsonString())
!= null);
}
/**
* Update permanent service coordinate. Note that this is a non-atomic operation with multiple
* trips to the backend system. The update is done in two operations; one delete and one
* create. If the delete operation fail and the create operation succeeds it might end up
* removing the permanent service coordinate. Clients will not be notified of the removal.
*
* @param coordinate The service's coordinate
* @param endpoint The service's endpoint
*/
public boolean updatePermanentService(
final ServiceCoordinate coordinate, final Endpoint endpoint) {
if (coordinate == null) {
throw new IllegalArgumentException("Coordinate can't be null");
}
if (endpoint == null) {
throw new IllegalArgumentException("Endpoint can't be null");
}
if (permanentUpdatesInProgress.contains(coordinate)) {
LOG.log(Level.WARNING, "Attempt to update a permanent service which is already"
+ " updating. (coordinate: " + coordinate + ", endpoint: " + endpoint);
return false;
}
// Check if the endpoint name still matches.
final String data = backend.readLeaseData(coordinate.toCloudnamePath());
if (data == null) {
return false;
}
final Endpoint oldEndpoint = Endpoint.fromJson(data);
if (!oldEndpoint.getName().equals(endpoint.getName())) {
LOG.log(Level.INFO, "Rejecting attempt to update permanent service with a new endpoint"
+ " that has a different name. Old name: " + oldEndpoint + " new: " + endpoint);
return false;
}
permanentUpdatesInProgress.add(coordinate);
try {
return backend.writeLeaseData(
coordinate.toCloudnamePath(), endpoint.toJsonString());
} catch (final RuntimeException ex) {
LOG.log(Level.WARNING, "Got exception updating permanent lease. The system might be in"
+ " an indeterminate state", ex);
return false;
} finally {
permanentUpdatesInProgress.remove(coordinate);
}
}
/**
* Remove a perviously registered permanent service. Needless to say: Use with caution.
*/
public boolean removePermanentService(final ServiceCoordinate coordinate) {
if (coordinate == null) {
throw new IllegalArgumentException("Coordinate can not be null");
}
return backend.removeLease(coordinate.toCloudnamePath());
}
/**
* Listen for changes in permanent services. The changes are usually of the earth-shattering
* variety so as a client you'd be interested in knowing about these as soon as possible.
*/
public void addPermanentServiceListener(
final ServiceCoordinate coordinate, final PermanentServiceListener listener) {
if (coordinate == null) {
throw new IllegalArgumentException("Coordinate can not be null");
}
if (listener == null) {
throw new IllegalArgumentException("Listener can not be null");
}
final LeaseListener leaseListener = new LeaseListener() {
@Override
public void leaseCreated(final CloudnamePath path, final String data) {
listener.onServiceCreated(Endpoint.fromJson(data));
}
@Override
public void leaseRemoved(final CloudnamePath path) {
listener.onServiceRemoved();
}
@Override
public void dataChanged(final CloudnamePath path, final String data) {
listener.onServiceChanged(Endpoint.fromJson(data));
}
};
synchronized (syncObject) {
permanentListeners.add(leaseListener);
}
backend.addLeaseListener(coordinate.toCloudnamePath(), leaseListener);
}
@Override
public void close() {
synchronized (syncObject) {
for (final ServiceHandle handle : handles) {
handle.close();
}
for (final LeaseListener listener : temporaryListeners) {
backend.removeLeaseListener(listener);
}
for (final LeaseListener listener : permanentListeners) {
backend.removeLeaseListener(listener);
}
}
}
}