/* * Copyright © 2014 Cask Data, Inc. * * 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 co.cask.cdap.data.stream; import co.cask.cdap.api.common.Bytes; import co.cask.cdap.api.data.schema.Schema; import co.cask.cdap.common.conf.PropertyChangeListener; import co.cask.cdap.common.conf.PropertyStore; import co.cask.cdap.common.conf.SyncPropertyUpdater; import co.cask.cdap.common.io.Codec; import co.cask.cdap.data2.transaction.stream.StreamConfig; import co.cask.cdap.internal.io.SchemaTypeAdapter; import co.cask.cdap.proto.Id; import com.google.common.base.Objects; import com.google.common.util.concurrent.AbstractIdleService; import com.google.common.util.concurrent.ListenableFuture; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import org.apache.twill.common.Cancellable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.concurrent.Callable; import java.util.concurrent.locks.Lock; import javax.annotation.Nullable; /** * Base implementation for {@link StreamCoordinatorClient}. */ public abstract class AbstractStreamCoordinatorClient extends AbstractIdleService implements StreamCoordinatorClient { private static final Logger LOG = LoggerFactory.getLogger(AbstractStreamCoordinatorClient.class); private static final Gson GSON = new GsonBuilder() .registerTypeAdapter(Schema.class, new SchemaTypeAdapter()).create(); private PropertyStore<CoordinatorStreamProperties> propertyStore; /** * Starts the service. * * @throws Exception when starting of the service failed */ protected abstract void doStartUp() throws Exception; /** * Stops the service. * * @throws Exception when stopping the service could not be performed */ protected abstract void doShutDown() throws Exception; /** * Creates a {@link PropertyStore}. * * @param codec Codec for the property stored in the property store * @param <T> Type of the property * @return A new {@link PropertyStore}. */ protected abstract <T> PropertyStore<T> createPropertyStore(Codec<T> codec); /** * Returns a {@link Lock} for performing exclusive operation for the given stream. */ protected abstract Lock getLock(Id.Stream streamId); /** * Gets invoked when a stream of the given name is created. */ protected abstract void streamCreated(Id.Stream streamId); /** * Gets invoked when a stream is deleted. */ protected abstract void streamDeleted(Id.Stream streamId); @Override public StreamConfig createStream(Id.Stream streamId, Callable<StreamConfig> action) throws Exception { Lock lock = getLock(streamId); lock.lock(); try { StreamConfig config = action.call(); if (config != null) { streamCreated(streamId); } return config; } finally { lock.unlock(); } } @Override public void updateProperties(Id.Stream streamId, Callable<CoordinatorStreamProperties> action) throws Exception { Lock lock = getLock(streamId); lock.lock(); try { updateProperties(streamId, action.call()).get(); } finally { lock.unlock(); } } @Override public void deleteStream(Id.Stream streamId, Runnable action) throws Exception { Lock lock = getLock(streamId); lock.lock(); try { action.run(); // TODO: CDAP-2161 Ideally would be deleting the property. However it is not supported by PropertyStore right now. propertyStore.set(streamId.toString(), null).get(); streamDeleted(streamId); } finally { lock.unlock(); } } @Override public <T> T exclusiveAction(Id.Stream streamId, Callable<T> action) throws Exception { Lock lock = getLock(streamId); lock.lock(); try { return action.call(); } finally { lock.unlock(); } } @Override public Cancellable addListener(Id.Stream streamId, StreamPropertyListener listener) { return propertyStore.addChangeListener(streamId.toString(), new StreamPropertyChangeListener(listener)); } @Override protected final void startUp() throws Exception { propertyStore = createPropertyStore(new Codec<CoordinatorStreamProperties>() { @Override public byte[] encode(CoordinatorStreamProperties properties) throws IOException { return Bytes.toBytes(GSON.toJson(properties)); } @Override public CoordinatorStreamProperties decode(byte[] data) throws IOException { return GSON.fromJson(Bytes.toString(data), CoordinatorStreamProperties.class); } }); try { doStartUp(); } catch (Exception e) { propertyStore.close(); throw e; } } @Override protected final void shutDown() throws Exception { propertyStore.close(); doShutDown(); } /** * Returns first if first is not {@code null}, otherwise return second. * It is different than Guava {@link Objects#firstNonNull(Object, Object)} in the way that it allows the second * parameter to be null. */ @Nullable private <T> T firstNotNull(@Nullable T first, @Nullable T second) { return first != null ? first : second; } /** * Updates stream properties in the property store. */ private ListenableFuture<CoordinatorStreamProperties> updateProperties(Id.Stream streamId, final CoordinatorStreamProperties properties) { return propertyStore.update(streamId.toString(), new SyncPropertyUpdater<CoordinatorStreamProperties>() { @Override protected CoordinatorStreamProperties compute(@Nullable CoordinatorStreamProperties oldProperties) { if (oldProperties == null) { return properties; } // Merge the old and new properties. return new CoordinatorStreamProperties( firstNotNull(properties.getTTL(), oldProperties.getTTL()), firstNotNull(properties.getFormat(), oldProperties.getFormat()), firstNotNull(properties.getNotificationThresholdMB(), oldProperties.getNotificationThresholdMB()), firstNotNull(properties.getGeneration(), oldProperties.getGeneration()), firstNotNull(properties.getDescription(), oldProperties.getDescription())); } }); } /** * A {@link PropertyChangeListener} that convert onChange callback into {@link StreamPropertyListener}. */ private final class StreamPropertyChangeListener extends StreamPropertyListener implements PropertyChangeListener<CoordinatorStreamProperties> { private final StreamPropertyListener listener; private CoordinatorStreamProperties oldProperties; private StreamPropertyChangeListener(StreamPropertyListener listener) { this.listener = listener; } @Override public void onChange(String name, CoordinatorStreamProperties properties) { Id.Stream streamId = Id.Stream.fromString(name, Id.Stream.class); if (properties == null) { deleted(streamId); oldProperties = null; return; } Integer generation = properties.getGeneration(); Integer oldGeneration = (oldProperties == null) ? null : oldProperties.getGeneration(); if (generation != null && (oldGeneration == null || generation > oldGeneration)) { generationChanged(streamId, generation); } Long ttl = properties.getTTL(); Long oldTTL = (oldProperties == null) ? null : oldProperties.getTTL(); if (ttl != null && !ttl.equals(oldTTL)) { ttlChanged(streamId, ttl); } Integer threshold = properties.getNotificationThresholdMB(); Integer oldThreshold = (oldProperties == null) ? null : oldProperties.getNotificationThresholdMB(); if (threshold != null && !threshold.equals(oldThreshold)) { thresholdChanged(streamId, threshold); } oldProperties = properties; } @Override public void onError(String name, Throwable failureCause) { LOG.error("Exception on PropertyChangeListener for stream {}", name, failureCause); } @Override public void generationChanged(Id.Stream streamId, int generation) { try { listener.generationChanged(streamId, generation); } catch (Throwable t) { LOG.error("Exception while calling StreamPropertyListener.generationChanged", t); } } @Override public void ttlChanged(Id.Stream streamId, long ttl) { try { listener.ttlChanged(streamId, ttl); } catch (Throwable t) { LOG.error("Exception while calling StreamPropertyListener.ttlChanged", t); } } @Override public void thresholdChanged(Id.Stream streamId, int threshold) { try { listener.thresholdChanged(streamId, threshold); } catch (Throwable t) { LOG.error("Exception while calling StreamPropertyListener.thresholdChanged", t); } } @Override public void deleted(Id.Stream streamId) { try { listener.deleted(streamId); } catch (Throwable t) { LOG.error("Exception while calling StreamPropertyListener.deleted", t); } } } }