/**
* Copyright 2012 Nikita Koksharov
*
* 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 com.corundumstudio.socketio.ack;
import io.netty.util.internal.PlatformDependent;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.corundumstudio.socketio.AckCallback;
import com.corundumstudio.socketio.Disconnectable;
import com.corundumstudio.socketio.MultiTypeAckCallback;
import com.corundumstudio.socketio.MultiTypeArgs;
import com.corundumstudio.socketio.SocketIOClient;
import com.corundumstudio.socketio.handler.ClientHead;
import com.corundumstudio.socketio.protocol.Packet;
import com.corundumstudio.socketio.scheduler.CancelableScheduler;
import com.corundumstudio.socketio.scheduler.SchedulerKey;
import com.corundumstudio.socketio.scheduler.SchedulerKey.Type;
public class AckManager implements Disconnectable {
class AckEntry {
final Map<Long, AckCallback<?>> ackCallbacks = PlatformDependent.newConcurrentHashMap();
final AtomicLong ackIndex = new AtomicLong(-1);
public long addAckCallback(AckCallback<?> callback) {
long index = ackIndex.incrementAndGet();
ackCallbacks.put(index, callback);
return index;
}
public Set<Long> getAckIndexes() {
return ackCallbacks.keySet();
}
public AckCallback<?> getAckCallback(long index) {
return ackCallbacks.get(index);
}
public AckCallback<?> removeCallback(long index) {
return ackCallbacks.remove(index);
}
public void initAckIndex(long index) {
ackIndex.compareAndSet(-1, index);
}
}
private static final Logger log = LoggerFactory.getLogger(AckManager.class);
private final Map<UUID, AckEntry> ackEntries = PlatformDependent.newConcurrentHashMap();
private final CancelableScheduler scheduler;
public AckManager(CancelableScheduler scheduler) {
super();
this.scheduler = scheduler;
}
public void initAckIndex(UUID sessionId, long index) {
AckEntry ackEntry = getAckEntry(sessionId);
ackEntry.initAckIndex(index);
}
private AckEntry getAckEntry(UUID sessionId) {
AckEntry ackEntry = ackEntries.get(sessionId);
if (ackEntry == null) {
ackEntry = new AckEntry();
AckEntry oldAckEntry = ackEntries.put(sessionId, ackEntry);
if (oldAckEntry != null) {
ackEntry = oldAckEntry;
}
}
return ackEntry;
}
@SuppressWarnings("unchecked")
public void onAck(SocketIOClient client, Packet packet) {
AckSchedulerKey key = new AckSchedulerKey(Type.ACK_TIMEOUT, client.getSessionId(), packet.getAckId());
scheduler.cancel(key);
AckCallback callback = removeCallback(client.getSessionId(), packet.getAckId());
if (callback == null) {
return;
}
if (callback instanceof MultiTypeAckCallback) {
callback.onSuccess(new MultiTypeArgs(packet.<List<Object>>getData()));
} else {
Object param = null;
List<Object> args = packet.getData();
if (!args.isEmpty()) {
param = args.get(0);
}
if (args.size() > 1) {
log.error("Wrong ack args amount. Should be only one argument, but current amount is: {}. Ack id: {}, sessionId: {}",
args.size(), packet.getAckId(), client.getSessionId());
}
callback.onSuccess(param);
}
}
private AckCallback<?> removeCallback(UUID sessionId, long index) {
AckEntry ackEntry = ackEntries.get(sessionId);
// may be null if client disconnected
// before timeout occurs
if (ackEntry != null) {
return ackEntry.removeCallback(index);
}
return null;
}
public AckCallback<?> getCallback(UUID sessionId, long index) {
AckEntry ackEntry = getAckEntry(sessionId);
return ackEntry.getAckCallback(index);
}
public long registerAck(UUID sessionId, AckCallback<?> callback) {
AckEntry ackEntry = getAckEntry(sessionId);
ackEntry.initAckIndex(0);
long index = ackEntry.addAckCallback(callback);
if (log.isDebugEnabled()) {
log.debug("AckCallback registered with id: {} for client: {}", index, sessionId);
}
scheduleTimeout(index, sessionId, callback);
return index;
}
private void scheduleTimeout(final long index, final UUID sessionId, AckCallback<?> callback) {
if (callback.getTimeout() == -1) {
return;
}
SchedulerKey key = new AckSchedulerKey(Type.ACK_TIMEOUT, sessionId, index);
scheduler.scheduleCallback(key, new Runnable() {
@Override
public void run() {
AckCallback<?> cb = removeCallback(sessionId, index);
if (cb != null) {
cb.onTimeout();
}
}
}, callback.getTimeout(), TimeUnit.SECONDS);
}
@Override
public void onDisconnect(ClientHead client) {
AckEntry e = ackEntries.remove(client.getSessionId());
if (e == null) {
return;
}
Set<Long> indexes = e.getAckIndexes();
for (Long index : indexes) {
AckCallback<?> callback = e.getAckCallback(index);
if (callback != null) {
callback.onTimeout();
}
SchedulerKey key = new AckSchedulerKey(Type.ACK_TIMEOUT, client.getSessionId(), index);
scheduler.cancel(key);
}
}
}