/*
* Copyright (c) 2008-2017, Hazelcast, Inc. All Rights Reserved.
*
* 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.hazelcast.client.spi.impl.listener;
import com.hazelcast.client.connection.ClientConnectionManager;
import com.hazelcast.client.connection.nio.ClientConnection;
import com.hazelcast.client.impl.HazelcastClientInstanceImpl;
import com.hazelcast.client.impl.protocol.ClientMessage;
import com.hazelcast.client.spi.ClientClusterService;
import com.hazelcast.client.spi.EventHandler;
import com.hazelcast.client.spi.impl.ClientInvocation;
import com.hazelcast.client.spi.impl.ClientInvocationFuture;
import com.hazelcast.client.spi.impl.ConnectionHeartbeatListener;
import com.hazelcast.client.spi.impl.ListenerMessageCodec;
import com.hazelcast.core.HazelcastException;
import com.hazelcast.core.Member;
import com.hazelcast.nio.Connection;
import com.hazelcast.nio.ConnectionListener;
import com.hazelcast.util.ExceptionUtil;
import com.hazelcast.util.UuidUtil;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class ClientSmartListenerService extends ClientListenerServiceImpl
implements ConnectionListener, ConnectionHeartbeatListener {
private final Map<ClientRegistrationKey, Map<Connection, ClientEventRegistration>> registrations
= new ConcurrentHashMap<ClientRegistrationKey, Map<Connection, ClientEventRegistration>>();
private final ClientConnectionManager clientConnectionManager;
private final Map<Connection, Collection<ClientRegistrationKey>> failedRegistrations
= new ConcurrentHashMap<Connection, Collection<ClientRegistrationKey>>();
public ClientSmartListenerService(HazelcastClientInstanceImpl client,
int eventThreadCount, int eventQueueCapacity) {
super(client, eventThreadCount, eventQueueCapacity);
clientConnectionManager = client.getConnectionManager();
}
@Override
public String registerListener(final ListenerMessageCodec codec, final EventHandler handler) {
//This method should not be called from registrationExecutor
assert (!Thread.currentThread().getName().contains("eventRegistration"));
Future<String> future = registrationExecutor.submit(new Callable<String>() {
@Override
public String call() {
String userRegistrationId = UuidUtil.newUnsecureUuidString();
ClientRegistrationKey registrationKey = new ClientRegistrationKey(userRegistrationId, handler, codec);
registrations.put(registrationKey, new ConcurrentHashMap<Connection, ClientEventRegistration>());
try {
Collection<ClientConnection> connections = clientConnectionManager.getActiveConnections();
for (ClientConnection connection : connections) {
invoke(registrationKey, connection);
}
} catch (Exception e) {
deregisterListenerInternal(userRegistrationId);
throw new HazelcastException("Listener can not be added", e);
}
return userRegistrationId;
}
});
try {
return future.get();
} catch (Exception e) {
throw ExceptionUtil.rethrow(e);
}
}
private void invoke(ClientRegistrationKey registrationKey, Connection connection) throws Exception {
//This method should only be called from registrationExecutor
assert (Thread.currentThread().getName().contains("eventRegistration"));
Map<Connection, ClientEventRegistration> registrationMap = registrations.get(registrationKey);
if (registrationMap.containsKey(connection)) {
return;
}
ListenerMessageCodec codec = registrationKey.getCodec();
ClientMessage request = codec.encodeAddRequest(true);
EventHandler handler = registrationKey.getHandler();
handler.beforeListenerRegister();
ClientInvocation invocation = new ClientInvocation(client, request, connection);
invocation.setEventHandler(handler);
ClientInvocationFuture future = invocation.invokeUrgent();
ClientMessage clientMessage;
try {
clientMessage = future.get();
} catch (Exception e) {
throw ExceptionUtil.rethrow(e, Exception.class);
}
String serverRegistrationId = codec.decodeAddResponse(clientMessage);
handler.onListenerRegister();
long correlationId = request.getCorrelationId();
ClientEventRegistration registration
= new ClientEventRegistration(serverRegistrationId, correlationId, connection, codec);
registrationMap.put(connection, registration);
}
@Override
public boolean deregisterListener(final String userRegistrationId) {
//This method should not be called from registrationExecutor
assert (!Thread.currentThread().getName().contains("eventRegistration"));
Future<Boolean> future = registrationExecutor.submit(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return deregisterListenerInternal(userRegistrationId);
}
});
try {
return future.get();
} catch (Exception e) {
throw ExceptionUtil.rethrow(e);
}
}
private Boolean deregisterListenerInternal(String userRegistrationId) {
//This method should only be called from registrationExecutor
assert (Thread.currentThread().getName().contains("eventRegistration"));
ClientRegistrationKey key = new ClientRegistrationKey(userRegistrationId);
Map<Connection, ClientEventRegistration> registrationMap = registrations.get(key);
if (registrationMap == null) {
return false;
}
boolean successful = true;
for (ClientEventRegistration registration : registrationMap.values()) {
Connection subscriber = registration.getSubscriber();
try {
ListenerMessageCodec listenerMessageCodec = registration.getCodec();
String serverRegistrationId = registration.getServerRegistrationId();
ClientMessage request = listenerMessageCodec.encodeRemoveRequest(serverRegistrationId);
new ClientInvocation(client, request, subscriber).invoke().get();
removeEventHandler(registration.getCallId());
registrationMap.remove(subscriber);
} catch (Exception e) {
successful = false;
logger.warning("Deregistration of listener with id " + userRegistrationId
+ " has failed to address " + subscriber.getEndPoint(), e);
}
}
if (successful) {
registrations.remove(key);
}
return successful;
}
@Override
public void start() {
clientConnectionManager.addConnectionListener(this);
clientConnectionManager.addConnectionHeartbeatListener(this);
final ClientClusterService clientClusterService = client.getClientClusterService();
registrationExecutor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
Collection<Member> memberList = clientClusterService.getMemberList();
for (Member member : memberList) {
try {
clientConnectionManager.getOrTriggerConnect(member.getAddress(), false);
} catch (IOException e) {
return;
}
}
}
}, 1, 1, TimeUnit.SECONDS);
}
@Override
public void connectionAdded(final Connection connection) {
//This method should not be called from registrationExecutor
assert (!Thread.currentThread().getName().contains("eventRegistration"));
registrationExecutor.submit(new Runnable() {
@Override
public void run() {
for (ClientRegistrationKey registrationKey : registrations.keySet()) {
invokeFromInternalThread(registrationKey, connection);
}
}
});
}
@Override
public void connectionRemoved(final Connection connection) {
//This method should not be called from registrationExecutor
assert (!Thread.currentThread().getName().contains("eventRegistration"));
registrationExecutor.submit(new Runnable() {
@Override
public void run() {
failedRegistrations.remove(connection);
for (Map<Connection, ClientEventRegistration> registrationMap : registrations.values()) {
ClientEventRegistration registration = registrationMap.remove(connection);
if (registration != null) {
removeEventHandler(registration.getCallId());
}
}
}
});
}
@Override
public void heartbeatResumed(final Connection connection) {
//This method should not be called from registrationExecutor
assert (!Thread.currentThread().getName().contains("eventRegistration"));
registrationExecutor.submit(new Runnable() {
@Override
public void run() {
Collection<ClientRegistrationKey> registrationKeys = failedRegistrations.get(connection);
for (ClientRegistrationKey registrationKey : registrationKeys) {
invokeFromInternalThread(registrationKey, connection);
}
}
});
}
private void invokeFromInternalThread(ClientRegistrationKey registrationKey, Connection connection) {
//This method should only be called from registrationExecutor
assert (Thread.currentThread().getName().contains("eventRegistration"));
try {
invoke(registrationKey, connection);
} catch (IOException e) {
Collection<ClientRegistrationKey> failedRegsToConnection = failedRegistrations.get(connection);
if (failedRegsToConnection == null) {
failedRegsToConnection = Collections.newSetFromMap(new HashMap<ClientRegistrationKey, Boolean>());
failedRegistrations.put(connection, failedRegsToConnection);
}
failedRegsToConnection.add(registrationKey);
} catch (Exception e) {
logger.warning("Listener " + registrationKey + " can not be added to a new connection: "
+ connection + ", reason: " + e.getMessage());
}
}
@Override
public void heartbeatStopped(Connection connection) {
//no op
}
//For Testing
public Collection<ClientEventRegistration> getActiveRegistrations(final String uuid) {
//This method should not be called from registrationExecutor
assert (!Thread.currentThread().getName().contains("eventRegistration"));
Future<Collection<ClientEventRegistration>> future = registrationExecutor.submit(
new Callable<Collection<ClientEventRegistration>>() {
@Override
public Collection<ClientEventRegistration> call() {
ClientRegistrationKey key = new ClientRegistrationKey(uuid);
Map<Connection, ClientEventRegistration> registrationMap = registrations.get(key);
if (registrationMap == null) {
return Collections.EMPTY_LIST;
}
LinkedList<ClientEventRegistration> activeRegistrations = new LinkedList<ClientEventRegistration>();
for (ClientEventRegistration registration : registrationMap.values()) {
activeRegistrations.add(registration);
}
return activeRegistrations;
}
});
try {
return future.get();
} catch (Exception e) {
throw ExceptionUtil.rethrow(e);
}
}
// used in tests
public Map<ClientRegistrationKey, Map<Connection, ClientEventRegistration>> getRegistrations() {
return registrations;
}
}