/*
* 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.usergrid.persistence.actorsystem;
import akka.actor.ActorSelection;
import akka.actor.Address;
import akka.actor.UntypedActor;
import akka.cluster.Cluster;
import akka.cluster.ClusterEvent;
import akka.cluster.Member;
import akka.cluster.MemberStatus;
import org.apache.commons.lang.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
/**
* Once notified of nodes, sends unique propertyValue requests to ClusterSingletonRouter via it's local proxy.
*/
public class ClientActor extends UntypedActor {
private static final Logger logger = LoggerFactory.getLogger( ClientActor.class );
private final String name = RandomStringUtils.randomAlphanumeric( 4 );
private final Set<Address> nodes = new HashSet<>();
private final Cluster cluster = Cluster.get(getContext().system());
private final Map<Class, String> routersByMessageType;
private boolean ready = false;
public ClientActor( Map<Class, String> routersByMessageType ) {
this.routersByMessageType = routersByMessageType;
}
// subscribe to cluster changes, MemberEvent
@Override
public void preStart() {
logger.debug("{} role {} address {}:{} starting up, subscribing to cluster events...", name,
cluster.getSelfRoles().iterator().next(),
cluster.readView().selfAddress().host(),
cluster.readView().selfAddress().hostPort());
cluster.subscribe(getSelf(), ClusterEvent.MemberEvent.class, ClusterEvent.ReachabilityEvent.class);
}
// re-subscribe when restart
@Override
public void postStop() {
cluster.unsubscribe(getSelf());
}
@Override
public void onReceive(Object message) {
int startSize = nodes.size();
String routerPath = routersByMessageType.get( message.getClass() );
if ( routerPath != null && ready ) {
// just pick any node, the ClusterSingletonRouter will do the consistent hash routing
List<Address> nodesList = new ArrayList<>( nodes );
Address address = nodesList.get( ThreadLocalRandom.current().nextInt( nodesList.size() ) );
ActorSelection service = getContext().actorSelection( address + routerPath );
service.tell( message, getSender() );
} else if ( routerPath != null ) {
logger.debug("{} responding with status unknown", name);
getSender().tell( new ErrorResponse("ClientActor not ready"), getSender() );
} else if ( message instanceof StatusRequest ) {
if ( ready ) {
getSender().tell( new StatusMessage( name, StatusMessage.Status.READY ), getSender() );
} else {
getSender().tell( new StatusMessage( name, StatusMessage.Status.INITIALIZING), getSender() );
}
return;
} else {
processAsClusterEvent( message );
}
if ( logger.isDebugEnabled() && startSize != nodes.size() ) {
logger.debug( "{} now knows {} nodes", name, nodes.size() );
}
if (!nodes.isEmpty() && !ready) {
logger.debug( name + " is ready" );
ready = true;
} else if (nodes.isEmpty() && ready) {
ready = false;
}
}
/**
* Process messages about nodes up, down, reachable and unreachable.
*/
private void processAsClusterEvent(Object message) {
if (message instanceof ClusterEvent.CurrentClusterState) {
ClusterEvent.CurrentClusterState state = (ClusterEvent.CurrentClusterState) message;
nodes.clear();
for (Member member : state.getMembers()) {
if (member.hasRole("io") && member.status().equals( MemberStatus.up())) {
nodes.add(member.address());
logger.debug("RequestActor {} received cluster-state member-up for {}", name, member.address());
}
}
} else if (message instanceof ClusterEvent.MemberUp) {
ClusterEvent.MemberUp mUp = (ClusterEvent.MemberUp) message;
if (mUp.member().hasRole("io")) {
nodes.add( mUp.member().address() );
}
logger.debug("{} received member-up for {}", name, mUp.member().address());
} else if (message instanceof ClusterEvent.MemberEvent) {
ClusterEvent.MemberEvent other = (ClusterEvent.MemberEvent) message;
nodes.remove(other.member().address());
} else if (message instanceof ClusterEvent.UnreachableMember) {
ClusterEvent.UnreachableMember unreachable = (ClusterEvent.UnreachableMember) message;
nodes.remove(unreachable.member().address());
logger.debug("{} received un-reachable for {}", name, unreachable.member().address());
} else if (message instanceof ClusterEvent.ReachableMember) {
ClusterEvent.ReachableMember reachable = (ClusterEvent.ReachableMember) message;
if (reachable.member().hasRole("io")) {
nodes.add( reachable.member().address() );
}
logger.debug("{} received reachable for {}", name, reachable.member().address());
} else {
logger.error("{}: unhandled message: {}", name, message.toString());
unhandled(message);
}
}
/**
* RequestAction responds to StatusRequests.
*/
public static class StatusRequest implements Serializable { }
/**
* RequestActor responds with, and some times unilaterally sends StatusMessages.
*/
public static class StatusMessage implements Serializable {
final String name;
public Status getStatus() {
return status;
}
public enum Status { INITIALIZING, READY }
private final Status status;
public StatusMessage(String name, Status status) {
this.name = name;
this.status = status;
}
public String getName() {
return name;
}
public boolean isReady() {
return status.equals( Status.READY );
}
}
public static class ErrorResponse implements Serializable {
private String message;
public ErrorResponse( String message ) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
}