package org.activityinfo.legacy.client.remote;
/*
* #%L
* ActivityInfo Server
* %%
* Copyright (C) 2009 - 2013 UNICEF
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.inject.Inject;
import org.activityinfo.legacy.client.Dispatcher;
import org.activityinfo.legacy.shared.Log;
import org.activityinfo.legacy.shared.command.Command;
import org.activityinfo.legacy.shared.command.result.CommandResult;
import org.activityinfo.legacy.shared.exception.CommandTimeoutException;
import org.activityinfo.legacy.shared.util.BackOff;
import org.activityinfo.legacy.shared.util.Commands;
import org.activityinfo.legacy.shared.util.ExponentialBackOff;
import org.activityinfo.server.endpoint.gwtrpc.AdvisoryLock;
import java.util.ArrayList;
import java.util.List;
/**
* An implementation of {@link org.activityinfo.legacy.client.Dispatcher} that
* merges equivalent commands independently executed during the same event loop.
* <p/>
* <p/>
* This is critical when we have multiple, independent components that all
* request GetSchema() or other basic information as they are loaded.
*/
public class MergingDispatcher extends AbstractDispatcher {
private Dispatcher dispatcher;
private Scheduler scheduler;
/**
* Pending commands have been requested but not yet sent to the server
*/
private List<CommandRequest> pendingCommands = new ArrayList<CommandRequest>();
/**
* Executing commands have been sent to the server but for which we have not
* yet received a response.
*/
private List<CommandRequest> executingCommands = new ArrayList<CommandRequest>();
private BackOff backOff;
@Inject
public MergingDispatcher(Dispatcher dispatcher, Scheduler scheduler) {
this(dispatcher, scheduler, new ExponentialBackOff.Builder()
.setInitialIntervalMillis(AdvisoryLock.ADVISORY_GET_LOCK_TIMEOUT * 1000)
.setMultiplier(2) // increase in 2 times
.build());
}
public MergingDispatcher(Dispatcher dispatcher, Scheduler scheduler, BackOff backOff) {
this.dispatcher = dispatcher;
this.scheduler = scheduler;
this.backOff = backOff;
scheduler.scheduleFinally(new RepeatingCommand() {
@Override
public boolean execute() {
try {
if (!pendingCommands.isEmpty()) {
dispatchPending();
}
} catch (Exception e) {
Log.error("Uncaught exception while dispatching in MergingDispatcher", e);
}
return true;
}
});
}
@Override
public <T extends CommandResult> void execute(Command<T> command, AsyncCallback<T> callback) {
CommandRequest request = new CommandRequest(command, callback);
if (Commands.hasMutatingCommand(command)) {
// mutating requests get queued immediately, don't try to merge them
// into any pending/executing commands, it wouldn't be correct
queue(request);
} else {
if (!request.mergeSuccessfulInto(pendingCommands) && !request.mergeSuccessfulInto(executingCommands)) {
queue(request);
Log.debug("MergingDispatcher: Scheduled " + command.toString() + ", now " +
pendingCommands.size() + " command(s) pending");
}
}
}
private void queue(CommandRequest request) {
pendingCommands.add(request);
}
private void dispatchPending() {
Log.debug("MergingDispatcher: sending " + pendingCommands.size() + " to server.");
final List<CommandRequest> sent = new ArrayList<CommandRequest>(pendingCommands);
executingCommands.addAll(sent);
pendingCommands.clear();
if (!sent.isEmpty()) {
for (final CommandRequest request : sent) {
executeCommand(request, new RetryCountDown(backOff));
}
}
}
private void executeCommand(final CommandRequest request, final RetryCountDown retryCountDown) {
dispatcher.execute(request.getCommand(), new AsyncCallback() {
@Override
public void onFailure(Throwable caught) {
if (caught instanceof CommandTimeoutException) {
Log.debug("Request timed out, retrying...");
retry(request, retryCountDown);
} else {
executingCommands.remove(request);
request.onFailure(caught);
}
}
@Override
public void onSuccess(Object result) {
executingCommands.remove(request);
request.onSuccess(result);
}
});
}
private void retry(final CommandRequest request, final RetryCountDown retryCountDown) {
try {
long waitPeriod = retryCountDown.countDownAndGetWaitPeriod();
Log.debug("Retry will run in " + waitPeriod + "ms.");
scheduler.scheduleFixedPeriod(new RepeatingCommand() {
@Override
public boolean execute() {
executeCommand(request, retryCountDown);
return false;
}
}, (int) waitPeriod);
} catch (Exception e) {
Log.error(e.getMessage(), e);
executingCommands.remove(request);
request.onFailure(e);
}
}
}