Clustering Tutorial: Survey

We're going to write a survey application and because we expect many users we are going to write it to run in a cluster in MASTER-SLAVE mode. This means that when we create our survey room on a node it will be created as a master room. All other nodes will automatically create a survey room running as slaves.

Application Design

As with any usual server side application we need to specify which module messages the client will send to the room and which messages the room will send to the client.

Room Messages to Client

QUESTION - sent with 1 argument that represents the text of the question

Client Module Messages to Room

RESPONSE - sent with 1 argument, response, that can be either "yes" or "no"

We are also building the application to run in a cluster. Our master room will act as the brain and control when and which question is asked. A good design philosophy is to limit the amount of communication between nodes. So while we could send a remote event from each slave to the master as every client answers instead we'll gather the information on the slaves and only send the master aggregates.

Our survey application will use the following remote events:

Master to Slave Remote Events

ASK_QUESTION - dispatched when slaves should ask a question to their clients
END_QUESTION - dispatched when the question response time is complete and slaves should send their aggregate response totals to the master

Slave to Master Remote Events

SURVEY_RESULTS - dispatched to report aggregate response totals to the master

Create the Remote Event Objects

Each event is dispatched with an object that must implement net.user1.union.core.event.RemoteEvent. The object, and any non-transient objects it contains, must implement either Serializable or Externalizable so they can be transported across the cluster.

Following is the source for our AskQuestionEvent remote event object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package net.user1.union.example.survey;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

import net.user1.union.core.event.BaseEvent;
import net.user1.union.core.event.RemoteEvent;

/**
 * The AskQuestionEvent is dispatched by the master when a new survey question is asked.
 */

public class AskQuestionEvent extends BaseEvent implements RemoteEvent, Externalizable {
    private String m_question;
   
    public AskQuestionEvent(String question) {
        m_question = question;
    }

    public String getQuestion() {
        return m_question;
    }

    public void readExternal(ObjectInput in)
    throws IOException, ClassNotFoundException {
        m_question = in.readUTF();
    }

    public void writeExternal(ObjectOutput out)
    throws IOException {
        out.writeUTF(m_question);
    }
}

Create the Master Room Class

Our master room module is the brains of the survey application. It controls the behaviour of the application across the cluster. It also takes the aggregates of all responses across the cluster and outputs the total to System.out.

When creating our room we won't actually pass this class as the module class since we want the room to use a different class if it is a master versus a slave. At the end we'll create a factory class to handle this for us.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
package net.user1.union.example.survey;

import net.user1.union.api.Module;
import net.user1.union.core.context.ModuleContext;
import net.user1.union.core.event.RemoteEvent;
import net.user1.union.core.event.RemoteRoomEvent;
import net.user1.union.core.event.RoomEvent;

/**
 * The class that is created when the local room is the MASTER room.
 */

public class SurveyMasterModule implements Module, Runnable {
    private ModuleContext m_ctx;
   
    // the game thread
    private Thread m_gameThread;
   
    // the questions to ask
    private String[] m_questions = {
            "Are you a programmer?",
            "Can you ice skate?",
            "Have you ever been to Australia?"
    };
   
    // results
    private int m_yes;
    private int m_no;
   
    /**
     * Invoked by the server when the room is being created and the module should initialize
     * itself
     */

    public boolean init(ModuleContext ctx) {
        m_ctx = ctx;
       
        m_gameThread = new Thread(this);
        m_gameThread.start();
       
        // listen for when a client has sent a response (it is sent with a module message)
        m_ctx.getRoom().addEventListener(RoomEvent.MODULE_MESSAGE, this, "onModuleMessage");
       
        // listen for when a new slave room as been added to the cluster so that we can
        // initialize that slave with the current state of the survey
        m_ctx.getRoom().addRemoteEventListener(RemoteRoomEvent.ADD_SLAVE_ROOM, this,
                "onAddSlaveRoom");
       
        // listen for when a slave room has sent us the results from clients connected to that
        // node
        m_ctx.getRoom().addRemoteEventListener("SURVEY_RESULTS", this, "onSurveyResults");
             
        return true;
    }
   
    public void run() {
        int currentQuestion = 0;
       
        while (m_gameThread != null) {
            // ask a question by dispatching a remote event containing the question
            // the event will automatically be sent to all nodes on the cluster
            // and dispatched by the slave instances of this room
            m_ctx.getRoom().dispatchRemoteEvent("ASK_QUESTION",
                    new AskQuestionEvent(m_questions[currentQuestion]));
           
            // and send to clients connected to our server
            // broadcast the question to the clients connected to this server
            m_ctx.getRoom().sendMessage("QUESTION", m_questions[currentQuestion]);
           
            // advance to the next question
            currentQuestion = (currentQuestion+1) % m_questions.length;
           
            // wait 10 seconds for people to answer
            try {
                Thread.sleep(10000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
           
            // then end the question
            m_ctx.getRoom().dispatchRemoteEvent("END_QUESTION", new EndQuestionEvent());
           
            // wait 5 seconds for results to come in
            try {
                Thread.sleep(5000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
           
            // output results to System.out and reset results
            System.out.println("Yes: " + m_yes + " No: " + m_no);
            synchronized (this) {
                m_yes = 0;
                m_no = 0;
            }
        }
    }
   
    /**
     * This method is invoked when a module message has been sent by a client to the room. We
     * use it to get responses from clients that are connected to this server. Responses
     * collected by clients connected to other servers will be collected by the remote event
     * "SURVEY_RESULTS".
     */

    public void onModuleMessage(RoomEvent evt) {
        if ("RESPONSE".equals(evt.getMessage().getMessageName())) {
            // add the response to our totals
            synchronized (this) {
                if ("yes".equals(evt.getMessage().getArg("response"))) {
                    m_yes++;
                } else if ("no".equals(evt.getMessage().getArg("response"))) {
                    m_no++;
                }
            }
        }
    }
   
    /**
     * This method is invoked when a slave room has dispatched its results to the master room.
     */

    public void onSurveyResults(RemoteEvent evt) {
        // get the event Object and increment yes and no
        SurveyResultsEvent event = (SurveyResultsEvent)evt;
        synchronized (this) {
            m_yes += event.getYes();
            m_no += event.getNo();
        }
    }
   
    /**
     * Invoked by the server when the room is being removed
     */

    public void shutdown() {
        // clean up event listeners
        m_ctx.getRoom().removeEventListener(RoomEvent.MODULE_MESSAGE, this, "onModuleMessage");
        m_ctx.getRoom().removeRemoteEventListener(RemoteRoomEvent.ADD_SLAVE_ROOM, this,
                "onAddSlaveRoom");
        m_ctx.getRoom().removeRemoteEventListener("SURVEY_RESULTS", this, "onSurveyResults");
       
        m_gameThread = null;
    }
}

Create the Slave Room Class

While the master room serves as the brain for the cluster wide application we still want to have our slave rooms process as much local client information as possible without having to resort to communicating with or passing it on to the master since that involves sending traffic through the cluster. In this simple case we can save a lot of network traffic by only sending the totals in an aggregate rather than passing through each client response to the master.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package net.user1.union.example.survey;

import net.user1.union.api.Module;
import net.user1.union.core.context.ModuleContext;
import net.user1.union.core.event.RemoteEvent;
import net.user1.union.core.event.RoomEvent;

/**
 * The class that is created when the local room is a SLAVE room.
 */

public class SurveySlaveModule implements Module {
    private ModuleContext m_ctx;
   
    // results
    private int m_yes;
    private int m_no;
   
    public boolean init(ModuleContext ctx) {
        m_ctx = ctx;
       
        // listen for when a client has sent a response (it is sent with a module message)
        m_ctx.getRoom().addEventListener(RoomEvent.MODULE_MESSAGE, this, "onModuleMessage");
       
        // listen for when the master room is asking a question
        m_ctx.getRoom().addRemoteEventListener("ASK_QUESTION", this, "onAskQuestion");
       
        // listen for when the master room has finished asking a question
        m_ctx.getRoom().addRemoteEventListener("END_QUESTION", this, "onEndQuestion");
       
        return true;
    }

    /**
     * This method is invoked when a module message has been sent by a client to the room. We
     * use it to get responses from clients that are connected to this server. Responses
     * collected by clients connected to other servers will be collected by the remote event
     * "SURVEY_RESULTS".
     */

    public void onModuleMessage(RoomEvent evt) {
        if ("RESPONSE".equals(evt.getMessage().getMessageName())) {
            // add the response to our totals
            synchronized (this) {
                if ("yes".equals(evt.getMessage().getArg("response"))) {
                    m_yes++;
                } else if ("no".equals(evt.getMessage().getArg("response"))) {
                    m_no++;
                }
            }
        }
    }
   
    /**
     * This method is invoked when the master room is asking a question.
     */

    public void onAskQuestion(RemoteEvent evt) {
        // a custom event so we have to cast it
        AskQuestionEvent event = (AskQuestionEvent)evt;
       
        // broadcast the question to the clients connected to this server
        m_ctx.getRoom().sendMessage("QUESTION", event.getQuestion());
    }

    /**
     * This method is invoked when the master room has finished asking a question.
     */

    public void onEndQuestion(RemoteEvent evt) {
        synchronized (this) {
            // send the results to the master room and reset results
            m_ctx.getRoom().dispatchRemoteEvent("SURVEY_RESULTS",
                    new SurveyResultsEvent(m_yes, m_no));
           
            m_yes = 0;
            m_no = 0;
        }
    }
   
    public void shutdown() {
        // clean up events
        m_ctx.getRoom().removeEventListener(RoomEvent.MODULE_MESSAGE, this, "onModuleMessage");
        m_ctx.getRoom().removeRemoteEventListener("ASK_QUESTION", this, "onAskQuestion");
        m_ctx.getRoom().removeRemoteEventListener("END_QUESTION", this, "onEndQuestion");
    }
}

Create a Room Module to Load the Master or Slave Code.

For this application we want our module in the slave rooms to run a different set of code than the module in the master room. So our room module acts as a simple factory and queries the room to determine if it is the master room or a slave and then loads the appropriate survey application code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package net.user1.union.example.survey;

import net.user1.union.api.Module;
import net.user1.union.cluster.ClusterRole;
import net.user1.union.core.context.ModuleContext;

/**
 * Module code to add the survey application to a room. This class is designed to work in a
 * scaled environment and will initialize a master or slave depending on its role within the
 * cluster. One node in the cluster will have an instance of the room with a ClusterRole of
 * MASTER (the node where the room was initially created). All other nodes in the cluster will
 * have an instance of the room with a ClusterRole of SLAVE.
 *
 * The survey application asks clients a "yes/no" question and then tallies the results of the
 * responses.
 */

public class SurveyRoomModule implements Module {
    private Module m_surveyModule;
   
    public boolean init(ModuleContext ctx) {
        // if it is a slave module then run the slave module code otherwise it is either being
        // deployed as a master or in a non-clustered environment and so we want the master code
        // to run
        if (ctx.getRoom().getClusterRole() == ClusterRole.SLAVE) {
            m_surveyModule = new SurveySlaveModule();
        } else {
            m_surveyModule = new SurveyMasterModule();
        }
       
        return m_surveyModule.init(ctx);
    }

    public void shutdown() {
        m_surveyModule.shutdown();
    }
}

Creating the Room

Now we can create our room just as we would in an non-clustered environment.

By configuring union.xml

Add the following entry under the <rooms> element. The room will be created when the server starts.

1
2
3
4
5
6
7
8
9
10
11
    <room>
        <id>SurveyTutorial</id>
        <attributes>
            <attribute name="_DIE_ON_EMPTY">false</attribute>
        </attributes>
        <modules>
            <module>
                 <source type="class">net.user1.union.example.survey.SurveyRoomModule</source>
            </module>                              
        </modules>
    </room>

Programmatically on the Server

Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Create the room def
RoomDef roomDef = new RoomDef();

// Set the ID of the room
roomDef.setRoomID("SurveyTutorial");

// Set the room attribute to not die on empty
AttributeDef attrDef = new AttributeDef();
attrDef.setName(LocalRoom.ATTR_DIE_ON_EMPTY);
attrDef.setValue(Boolean.FALSE);
attrDef.setFlags(Attribute.FLAG_SHARED);
roomDef.addAttribute(attrDef);
     
// Add the room module            
ModuleDef modDef = new ModuleDef();
modDef.setType(ModuleDef.CLASS);
modDef.setSource("net.user1.union.example.survey.SurveyRoomModule");
roomDef.addModule(modDef);
       
// Use the room context that was passed to your module to get a reference to the server and create our room        
Room room = roomContext.getServer().createRoom(roomDef);