/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.brooklyn.core.feed;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import org.apache.brooklyn.api.entity.EntityLocal;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.mgmt.BrooklynTaskTags;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.task.DynamicSequentialTask;
import org.apache.brooklyn.util.core.task.ScheduledTask;
import org.apache.brooklyn.util.core.task.TaskTags;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Objects;
/**
* For executing periodic polls.
* Jobs are added to the schedule, and then the poller is started.
* The jobs will then be executed periodically, and the handler called for the result/failure.
*
* Assumes the schedule+start will be done single threaded, and that stop will not be done concurrently.
*/
public class Poller<V> {
public static final Logger log = LoggerFactory.getLogger(Poller.class);
private final EntityLocal entity;
private final boolean onlyIfServiceUp;
private final Set<Callable<?>> oneOffJobs = new LinkedHashSet<Callable<?>>();
private final Set<PollJob<V>> pollJobs = new LinkedHashSet<PollJob<V>>();
private final Set<Task<?>> oneOffTasks = new LinkedHashSet<Task<?>>();
private final Set<ScheduledTask> tasks = new LinkedHashSet<ScheduledTask>();
private volatile boolean started = false;
private static class PollJob<V> {
final PollHandler<? super V> handler;
final Duration pollPeriod;
final Runnable wrappedJob;
private boolean loggedPreviousException = false;
PollJob(final Callable<V> job, final PollHandler<? super V> handler, Duration period) {
this.handler = handler;
this.pollPeriod = period;
wrappedJob = new Runnable() {
public void run() {
try {
V val = job.call();
loggedPreviousException = false;
if (handler.checkSuccess(val)) {
handler.onSuccess(val);
} else {
handler.onFailure(val);
}
} catch (Exception e) {
if (loggedPreviousException) {
if (log.isTraceEnabled()) log.trace("PollJob for {}, repeated consecutive failures, handling {} using {}", new Object[] {job, e, handler});
} else {
if (log.isDebugEnabled()) log.debug("PollJob for {} handling {} using {}", new Object[] {job, e, handler});
loggedPreviousException = true;
}
handler.onException(e);
}
}
};
}
}
/** @deprecated since 0.7.0, pass in whether should run onlyIfServiceUp */
@Deprecated
public Poller(EntityLocal entity) {
this(entity, false);
}
public Poller(EntityLocal entity, boolean onlyIfServiceUp) {
this.entity = entity;
this.onlyIfServiceUp = onlyIfServiceUp;
}
/** Submits a one-off poll job; recommended that callers supply to-String so that task has a decent description */
public void submit(Callable<?> job) {
if (started) {
throw new IllegalStateException("Cannot submit additional tasks after poller has started");
}
oneOffJobs.add(job);
}
public void scheduleAtFixedRate(Callable<V> job, PollHandler<? super V> handler, long period) {
scheduleAtFixedRate(job, handler, Duration.millis(period));
}
public void scheduleAtFixedRate(Callable<V> job, PollHandler<? super V> handler, Duration period) {
if (started) {
throw new IllegalStateException("Cannot schedule additional tasks after poller has started");
}
PollJob<V> foo = new PollJob<V>(job, handler, period);
pollJobs.add(foo);
}
@SuppressWarnings({ "unchecked" })
public void start() {
// TODO Previous incarnation of this logged this logged polledSensors.keySet(), but we don't know that anymore
// Is that ok, are can we do better?
if (log.isDebugEnabled()) log.debug("Starting poll for {} (using {})", new Object[] {entity, this});
if (started) {
throw new IllegalStateException(String.format("Attempt to start poller %s of entity %s when already running",
this, entity));
}
started = true;
for (final Callable<?> oneOffJob : oneOffJobs) {
Task<?> task = Tasks.builder().dynamic(false).body((Callable<Object>) oneOffJob).displayName("Poll").description("One-time poll job "+oneOffJob).build();
oneOffTasks.add(((EntityInternal)entity).getExecutionContext().submit(task));
}
for (final PollJob<V> pollJob : pollJobs) {
final String scheduleName = pollJob.handler.getDescription();
if (pollJob.pollPeriod.compareTo(Duration.ZERO) > 0) {
Callable<Task<?>> pollingTaskFactory = new Callable<Task<?>>() {
public Task<?> call() {
DynamicSequentialTask<Void> task = new DynamicSequentialTask<Void>(MutableMap.of("displayName", scheduleName, "entity", entity),
new Callable<Void>() { public Void call() {
if (onlyIfServiceUp && !Boolean.TRUE.equals(entity.getAttribute(Attributes.SERVICE_UP))) {
return null;
}
pollJob.wrappedJob.run();
return null;
} } );
BrooklynTaskTags.setTransient(task);
return task;
}
};
Map<String, ?> taskFlags = MutableMap.of("displayName", "scheduled:" + scheduleName);
ScheduledTask task = new ScheduledTask(taskFlags, pollingTaskFactory)
.period(pollJob.pollPeriod)
.cancelOnException(false);
tasks.add(Entities.submit(entity, task));
} else {
if (log.isDebugEnabled()) log.debug("Activating poll (but leaving off, as period {}) for {} (using {})", new Object[] {pollJob.pollPeriod, entity, this});
}
}
}
public void stop() {
if (log.isDebugEnabled()) log.debug("Stopping poll for {} (using {})", new Object[] {entity, this});
if (!started) {
throw new IllegalStateException(String.format("Attempt to stop poller %s of entity %s when not running",
this, entity));
}
started = false;
for (Task<?> task : oneOffTasks) {
if (task != null) task.cancel(true);
}
for (ScheduledTask task : tasks) {
if (task != null) task.cancel();
}
oneOffTasks.clear();
tasks.clear();
}
public boolean isRunning() {
boolean hasActiveTasks = false;
for (Task<?> task: tasks) {
if (task.isBegun() && !task.isDone()) {
hasActiveTasks = true;
break;
}
}
if (!started && hasActiveTasks) {
log.warn("Poller should not be running, but has active tasks, tasks: "+tasks);
}
return started && hasActiveTasks;
}
protected boolean isEmpty() {
return pollJobs.isEmpty();
}
public String toString() {
return Objects.toStringHelper(this).add("entity", entity).toString();
}
}