Repository: pinpoint-apm/pinpoint
Issue: Sending server meta data should be separated from the agent's lifecycle
Labels: enhancement

The agent's lifecycle (and sending agent information) is currently controlled by plugins. Each plugin must specify if it will control the agent's lifecycle, and call `Agent.start()` manually if it does.

For example, a Tomcat 6 application is modified to invoke `Agent.start()` after `org.apache.catalina.core.StandardService.start()` method completes, and only then would the agent sends its information along with the server's meta data.

The agent's information should be sent regardless of the enclosing application's status, and this requires adding a way for the application to notify that it has finished gathering the server's meta data.


Issue comments:
Making further modifications, It would be better for `PinpointBootstrap` to govern the agent's lifecycle (ie. start/stop methods).


Pull request review comments:

Patch that resolved the issue:
diff --git a/bootstrap/src/main/java/com/navercorp/pinpoint/bootstrap/context/ServerMetaDataHolder.java b/bootstrap/src/main/java/com/navercorp/pinpoint/bootstrap/context/ServerMetaDataHolder.java
index e6be6dc7f6a0..a46ba77f9498 100644
--- a/bootstrap/src/main/java/com/navercorp/pinpoint/bootstrap/context/ServerMetaDataHolder.java
+++ b/bootstrap/src/main/java/com/navercorp/pinpoint/bootstrap/context/ServerMetaDataHolder.java
@@ -22,11 +22,22 @@
  * @author hyungil.jeong
  */
 public interface ServerMetaDataHolder {
+    
     void setServerName(String serverName);
     
     void addConnector(String protocol, int port);
     
     void addServiceInfo(String serviceName, List<String> serviceLibs);
     
-    ServerMetaData getServerMetaData();
+    void addListener(ServerMetaDataListener listener);
+    
+    void removeListener(ServerMetaDataListener listener);
+    
+    void publishServerMetaData();
+    
+    public interface ServerMetaDataListener {
+        
+        public void publishServerMetaData(ServerMetaData serverMetaData);
+        
+    }
 }
diff --git a/plugins/tomcat/src/main/java/com/navercorp/pinpoint/plugin/tomcat/interceptor/StandardServiceStartInterceptor.java b/plugins/tomcat/src/main/java/com/navercorp/pinpoint/plugin/tomcat/interceptor/StandardServiceStartInterceptor.java
index 7ba9c3e1ee95..0be81c2beee8 100644
--- a/plugins/tomcat/src/main/java/com/navercorp/pinpoint/plugin/tomcat/interceptor/StandardServiceStartInterceptor.java
+++ b/plugins/tomcat/src/main/java/com/navercorp/pinpoint/plugin/tomcat/interceptor/StandardServiceStartInterceptor.java
@@ -51,5 +51,7 @@ public void after(Object target, Object[] args, Object result, Throwable throwab
 
         String serverInfo = ServerInfo.getServerInfo();
         this.traceContext.getServerMetaDataHolder().setServerName(serverInfo);
+        // Service started. Publish server meta data.
+        this.traceContext.getServerMetaDataHolder().publishServerMetaData();
     }
 }
diff --git a/profiler/src/main/java/com/navercorp/pinpoint/profiler/AgentInfoSender.java b/profiler/src/main/java/com/navercorp/pinpoint/profiler/AgentInfoSender.java
index b8e41f5a55c8..c8f76716f23a 100644
--- a/profiler/src/main/java/com/navercorp/pinpoint/profiler/AgentInfoSender.java
+++ b/profiler/src/main/java/com/navercorp/pinpoint/profiler/AgentInfoSender.java
@@ -30,7 +30,7 @@
 import org.slf4j.LoggerFactory;
 
 import com.navercorp.pinpoint.bootstrap.context.ServerMetaData;
-import com.navercorp.pinpoint.bootstrap.context.ServerMetaDataHolder;
+import com.navercorp.pinpoint.bootstrap.context.ServerMetaDataHolder.ServerMetaDataListener;
 import com.navercorp.pinpoint.bootstrap.context.ServiceInfo;
 import com.navercorp.pinpoint.common.Version;
 import com.navercorp.pinpoint.common.util.PinpointThreadFactory;
@@ -45,7 +45,7 @@
  * @author koo.taejin
  * @author hyungil.jeong
  */
-public class AgentInfoSender {
+public class AgentInfoSender implements ServerMetaDataListener {
     private static final Logger LOGGER = LoggerFactory.getLogger(AgentInfoSender.class);
 
     private static final ThreadFactory THREAD_FACTORY = new PinpointThreadFactory("Pinpoint-agentInfo-sender", true);
@@ -54,9 +54,8 @@ public class AgentInfoSender {
     private final long agentInfoSendIntervalMs; 
     private final EnhancedDataSender dataSender;
     private final AgentInformation agentInformation;
-    private final ServerMetaDataHolder serverMetaDataHolder;
 
-    public AgentInfoSender(EnhancedDataSender dataSender, long agentInfoSendIntervalMs, AgentInformation agentInformation, ServerMetaDataHolder serverMetaDataHolder) {
+    public AgentInfoSender(EnhancedDataSender dataSender, long agentInfoSendIntervalMs, AgentInformation agentInformation) {
         if (dataSender == null) {
             throw new NullPointerException("dataSender must not be null");
         }
@@ -66,7 +65,6 @@ public AgentInfoSender(EnhancedDataSender dataSender, long agentInfoSendInterval
         this.agentInfoSendIntervalMs = agentInfoSendIntervalMs;
         this.dataSender = dataSender;
         this.agentInformation = agentInformation;
-        this.serverMetaDataHolder = serverMetaDataHolder;
     }
 
     public void start() {
@@ -74,20 +72,43 @@ public void start() {
         if (LOGGER.isInfoEnabled()) {
             LOGGER.info("AgentInfoSender started. Sending startup information to Pinpoint server via {}. agentInfo={}", dataSender.getClass().getSimpleName(), agentInfo);
         }
+        send(agentInfo);
+    }
+    
+    @Override
+    public void publishServerMetaData(ServerMetaData serverMetaData) {
+        final TAgentInfo agentInfo = createTAgentInfo(serverMetaData);
+        if (LOGGER.isInfoEnabled()) {
+            LOGGER.info("Sending AgentInfo with ServerMetaData. {}", agentInfo);
+        }
+        send(agentInfo);
+    }
+    
+    private void send(final TAgentInfo agentInfo) {
         final AgentInfoSendRunnable agentInfoSendJob = new AgentInfoSendRunnable(agentInfo);
         new AgentInfoSendRunnableWrapper(agentInfoSendJob).repeatWithFixedDelay(EXECUTOR_SERVICE, 0, this.agentInfoSendIntervalMs, TimeUnit.MILLISECONDS);
     }
     
     private TAgentInfo createTAgentInfo() {
-        final ServerMetaData serverMetaData = this.serverMetaDataHolder.getServerMetaData();
-        
+        final TAgentInfo agentInfo = new TAgentInfo();
+        agentInfo.setIp(this.agentInformation.getHostIp());
+        agentInfo.setHostname(this.agentInformation.getMachineName());
+        agentInfo.setPorts("");
+        agentInfo.setAgentId(this.agentInformation.getAgentId());
+        agentInfo.setApplicationName(this.agentInformation.getApplicationName());
+        agentInfo.setPid(this.agentInformation.getPid());
+        agentInfo.setStartTimestamp(this.agentInformation.getStartTime());
+        agentInfo.setServiceType(this.agentInformation.getServerType().getCode());
+        agentInfo.setVersion(Version.VERSION);
+        return agentInfo;
+    }
+    
+    private TAgentInfo createTAgentInfo(final ServerMetaData serverMetaData) {
         final StringBuilder ports = new StringBuilder();
-
         for (Entry<Integer, String> entry : serverMetaData.getConnectors().entrySet()) {
             ports.append(" ");
             ports.append(entry.getKey());
         }
-        
         final TAgentInfo agentInfo = new TAgentInfo();
         agentInfo.setIp(this.agentInformation.getHostIp());
         agentInfo.setHostname(this.agentInformation.getMachineName());
@@ -98,9 +119,7 @@ private TAgentInfo createTAgentInfo() {
         agentInfo.setStartTimestamp(this.agentInformation.getStartTime());
         agentInfo.setServiceType(this.agentInformation.getServerType().getCode());
         agentInfo.setVersion(Version.VERSION);
-
         agentInfo.setServerMetaData(createTServiceInfo(serverMetaData));
-        
         return agentInfo;
     }
     
diff --git a/profiler/src/main/java/com/navercorp/pinpoint/profiler/DefaultAgent.java b/profiler/src/main/java/com/navercorp/pinpoint/profiler/DefaultAgent.java
index d490ed62a8e0..1a6a5cc1beef 100644
--- a/profiler/src/main/java/com/navercorp/pinpoint/profiler/DefaultAgent.java
+++ b/profiler/src/main/java/com/navercorp/pinpoint/profiler/DefaultAgent.java
@@ -152,9 +152,9 @@ public DefaultAgent(String agentArgs, Instrumentation instrumentation, ProfilerC
         List<DefaultProfilerPluginContext> pluginContexts = loadProfilerPlugins(profilerConfig, pluginJars);
 
         String applicationServerTypeString = profilerConfig.getApplicationServerType();
-        ServiceType applicationServerType = serviceTypeRegistryService.findServiceTypeByName(applicationServerTypeString);
+        ServiceType applicationServerType = this.serviceTypeRegistryService.findServiceTypeByName(applicationServerTypeString);
 
-        final ApplicationServerTypeResolver typeResolver = new ApplicationServerTypeResolver(pluginContexts, applicationServerType, serviceTypeRegistryService);
+        final ApplicationServerTypeResolver typeResolver = new ApplicationServerTypeResolver(pluginContexts, applicationServerType, this.serviceTypeRegistryService);
         if (!typeResolver.resolve()) {
             throw new PinpointException("ApplicationServerType not found.");
         }
@@ -182,7 +182,8 @@ public DefaultAgent(String agentArgs, Instrumentation instrumentation, ProfilerC
 
         this.traceContext = createTraceContext(agentInformation.getServerType());
 
-        this.agentInfoSender = new AgentInfoSender(tcpDataSender, profilerConfig.getAgentInfoSendRetryInterval(), this.agentInformation, this.serverMetaDataHolder);
+        this.agentInfoSender = new AgentInfoSender(tcpDataSender, profilerConfig.getAgentInfoSendRetryInterval(), this.agentInformation);
+        this.serverMetaDataHolder.addListener(this.agentInfoSender);
 
         this.agentStatMonitor = new AgentStatMonitor(this.statDataSender, this.agentInformation.getAgentId(), this.agentInformation.getStartTime());
         
@@ -195,14 +196,7 @@ public DefaultAgent(String agentArgs, Instrumentation instrumentation, ProfilerC
 
         preLoadClass();
 
-        /**
-         * FIXME
-         * In case of Tomcat, com.navercorp.pinpoint.profiler.modifier.tomcat.interceptor.CatalinaAwaitInterceptor invokes start() method 
-         * before entering await() method of org.apache.catalina.startup.Catalina. But for other applications, it must be invoked directly. 
-         */
-        if (typeResolver.isManuallyStartupRequired()) {
-            start();
-        }
+        start();
     }
 
     private CommandDispatcher createCommandDispatcher() {
diff --git a/profiler/src/main/java/com/navercorp/pinpoint/profiler/context/DefaultServerMetaDataHolder.java b/profiler/src/main/java/com/navercorp/pinpoint/profiler/context/DefaultServerMetaDataHolder.java
index 55effe32be25..0b9f34830219 100644
--- a/profiler/src/main/java/com/navercorp/pinpoint/profiler/context/DefaultServerMetaDataHolder.java
+++ b/profiler/src/main/java/com/navercorp/pinpoint/profiler/context/DefaultServerMetaDataHolder.java
@@ -33,6 +33,8 @@
  * @author hyungil.jeong
  */
 public class DefaultServerMetaDataHolder implements ServerMetaDataHolder {
+    
+    private final List<ServerMetaDataListener> listeners;
 
     protected String serverName;
     private final List<String> vmArgs;
@@ -40,6 +42,7 @@ public class DefaultServerMetaDataHolder implements ServerMetaDataHolder {
     protected final Queue<ServiceInfo> serviceInfos = new ConcurrentLinkedQueue<ServiceInfo>();
 
     public DefaultServerMetaDataHolder(List<String> vmArgs) {
+        this.listeners = new ArrayList<ServerMetaDataListener>();
         this.vmArgs = vmArgs;
     }
 
@@ -60,7 +63,24 @@ public void addServiceInfo(String serviceName, List<String> serviceLibs) {
     }
 
     @Override
-    public ServerMetaData getServerMetaData() {
+    public void addListener(ServerMetaDataListener listener) {
+        this.listeners.add(listener);
+    }
+
+    @Override
+    public void removeListener(ServerMetaDataListener listener) {
+        this.listeners.remove(listener);
+    }
+
+    @Override
+    public void publishServerMetaData() {
+        final ServerMetaData serverMetaData = createServerMetaData();
+        for (ServerMetaDataListener listener : this.listeners) {
+            listener.publishServerMetaData(serverMetaData);
+        }
+    }
+
+    private ServerMetaData createServerMetaData() {
         String serverName = this.serverName == null ? "" : this.serverName;
         List<String> vmArgs = 
                 this.vmArgs == null ? Collections.<String>emptyList() : new ArrayList<String>(this.vmArgs);
diff --git a/profiler/src/main/java/com/navercorp/pinpoint/profiler/util/ApplicationServerTypeResolver.java b/profiler/src/main/java/com/navercorp/pinpoint/profiler/util/ApplicationServerTypeResolver.java
index b9b9b7d74b03..6230c47635c3 100644
--- a/profiler/src/main/java/com/navercorp/pinpoint/profiler/util/ApplicationServerTypeResolver.java
+++ b/profiler/src/main/java/com/navercorp/pinpoint/profiler/util/ApplicationServerTypeResolver.java
@@ -19,12 +19,12 @@
 import java.util.ArrayList;
 import java.util.List;
 
-import com.navercorp.pinpoint.common.service.ServiceTypeRegistryService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.navercorp.pinpoint.bootstrap.plugin.ServerTypeDetector;
 import com.navercorp.pinpoint.common.ServiceType;
+import com.navercorp.pinpoint.common.service.ServiceTypeRegistryService;
 import com.navercorp.pinpoint.profiler.plugin.DefaultProfilerPluginContext;
 
 /**
@@ -40,27 +40,19 @@ public class ApplicationServerTypeResolver {
     private final List<ServerTypeDetector> detectors = new ArrayList<ServerTypeDetector>();
 
     private final ServiceTypeRegistryService serviceTypeRegistryService;
-    /**
-     * If we have to invoke startup() during agent initialization.
-     * Some service types like BLOC or STAND_ALONE don't have an acceptor to do this.
-     */
-    private boolean manuallyStartupRequired = true;
 
     public ApplicationServerTypeResolver(List<DefaultProfilerPluginContext> plugins, ServiceType defaultType, ServiceTypeRegistryService serviceTypeRegistryService) {
         if (serviceTypeRegistryService == null) {
             throw new NullPointerException("serviceTypeRegistryService must not be null");
         }
-
+        this.serviceTypeRegistryService = serviceTypeRegistryService;
         this.defaultType = defaultType;
         
         for (DefaultProfilerPluginContext context : plugins) {
             detectors.addAll(context.getServerTypeDetectors());
         }
-
-        this.serviceTypeRegistryService = serviceTypeRegistryService;
     }
 
-
     public String[] getServerLibPath() {
         return new String[0];
     }
@@ -69,10 +61,6 @@ public ServiceType getServerType() {
         return serverType;
     }
     
-    public boolean isManuallyStartupRequired() {
-        return manuallyStartupRequired;
-    }
-    
     public boolean resolve() {
         String serverType = null;
 


Test patch:
diff --git a/profiler/src/main/java/com/navercorp/pinpoint/test/PluginTestAgent.java b/profiler/src/main/java/com/navercorp/pinpoint/test/PluginTestAgent.java
index 45783d9ba5b1..2eb9896f4611 100644
--- a/profiler/src/main/java/com/navercorp/pinpoint/test/PluginTestAgent.java
+++ b/profiler/src/main/java/com/navercorp/pinpoint/test/PluginTestAgent.java
@@ -54,6 +54,8 @@
  */
 public class PluginTestAgent extends DefaultAgent implements PluginTestVerifier {
     
+    private TestableServerMetaDataListener serverMetaDataListener;
+    
     public PluginTestAgent(String agentArgs, Instrumentation instrumentation, ProfilerConfig profilerConfig, URL[] pluginJars) {
         super(agentArgs, instrumentation, profilerConfig, pluginJars);
         PluginTestVerifierHolder.setInstance(this);
@@ -88,7 +90,10 @@ protected EnhancedDataSender createTcpDataSender(CommandDispatcher commandDispat
     @Override
     protected ServerMetaDataHolder createServerMetaDataHolder() {
         List<String> vmArgs = RuntimeMXBeanUtils.getVmArgs();
-        return new ResettableServerMetaDataHolder(vmArgs);
+        ServerMetaDataHolder serverMetaDataHolder = new ResettableServerMetaDataHolder(vmArgs);
+        this.serverMetaDataListener = new TestableServerMetaDataListener();
+        serverMetaDataHolder.addListener(this.serverMetaDataListener);
+        return serverMetaDataHolder;
     }
     
     @Override
@@ -102,7 +107,7 @@ public void verifyServerType(ServiceType serviceType) {
     
     @Override
     public void verifyServerInfo(String expected) {
-        String actualName = getTraceContext().getServerMetaDataHolder().getServerMetaData().getServerInfo();
+        String actualName = this.serverMetaDataListener.getServerMetaData().getServerInfo();
         
         if (!actualName.equals(expected)) {
             throw new AssertionError("Expected server name [" + expected + "] but was [" + actualName + "]");
@@ -111,7 +116,7 @@ public void verifyServerInfo(String expected) {
 
     @Override
     public void verifyConnector(String protocol, int port) {
-        Map<Integer, String> connectorMap = getTraceContext().getServerMetaDataHolder().getServerMetaData().getConnectors();
+        Map<Integer, String> connectorMap = this.serverMetaDataListener.getServerMetaData().getConnectors();
         String actualProtocol = connectorMap.get(port);
         
         if (actualProtocol == null || !actualProtocol.equals(protocol)) {
@@ -121,7 +126,7 @@ public void verifyConnector(String protocol, int port) {
 
     @Override
     public void verifyService(String name, List<String> libs) {
-        List<ServiceInfo> serviceInfos = getTraceContext().getServerMetaDataHolder().getServerMetaData().getServiceInfos();
+        List<ServiceInfo> serviceInfos = this.serverMetaDataListener.getServerMetaData().getServiceInfos();
         
         for (ServiceInfo serviceInfo : serviceInfos) {
             if (serviceInfo.getServiceName().equals(name)) {
diff --git a/profiler/src/main/java/com/navercorp/pinpoint/test/TestableServerMetaDataListener.java b/profiler/src/main/java/com/navercorp/pinpoint/test/TestableServerMetaDataListener.java
new file mode 100644
index 000000000000..7795c982f102
--- /dev/null
+++ b/profiler/src/main/java/com/navercorp/pinpoint/test/TestableServerMetaDataListener.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2015 NAVER Corp.
+ *
+ * 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.navercorp.pinpoint.test;
+
+import com.navercorp.pinpoint.bootstrap.context.ServerMetaData;
+import com.navercorp.pinpoint.bootstrap.context.ServerMetaDataHolder.ServerMetaDataListener;
+
+/**
+ * @author HyunGil Jeong
+ */
+public class TestableServerMetaDataListener implements ServerMetaDataListener {
+    
+    private volatile ServerMetaData serverMetaData;
+
+    @Override
+    public void publishServerMetaData(ServerMetaData serverMetaData) {
+        this.serverMetaData = serverMetaData;
+    }
+    
+    public ServerMetaData getServerMetaData() {
+        return this.serverMetaData;
+    }
+    
+}
diff --git a/profiler/src/main/java/com/navercorp/pinpoint/test/junit4/BasePinpointTest.java b/profiler/src/main/java/com/navercorp/pinpoint/test/junit4/BasePinpointTest.java
index 9681fa517537..d32b53ed172b 100644
--- a/profiler/src/main/java/com/navercorp/pinpoint/test/junit4/BasePinpointTest.java
+++ b/profiler/src/main/java/com/navercorp/pinpoint/test/junit4/BasePinpointTest.java
@@ -23,6 +23,8 @@
 import com.navercorp.pinpoint.test.ListenableDataSender;
 import com.navercorp.pinpoint.test.MockAgent;
 import com.navercorp.pinpoint.test.ResettableServerMetaDataHolder;
+import com.navercorp.pinpoint.test.TestableServerMetaDataListener;
+
 import org.apache.thrift.TBase;
 import org.junit.runner.RunWith;
 
@@ -42,6 +44,7 @@ public abstract class BasePinpointTest {
 
     private volatile TBaseRecorder<? extends TBase<?, ?>> tBaseRecorder;
     private volatile ServerMetaDataHolder serverMetaDataHolder;
+    private final TestableServerMetaDataListener listener = new TestableServerMetaDataListener();
 
     protected List<SpanEventBo> getCurrentSpanEvents() {
         List<SpanEventBo> spanEvents = new ArrayList<SpanEventBo>();
@@ -65,7 +68,7 @@ protected List<SpanBo> getCurrentRootSpans() {
     }
     
     protected ServerMetaData getServerMetaData() {
-        return this.serverMetaDataHolder.getServerMetaData();
+        return this.listener.getServerMetaData();
     }
 
     private void setTBaseRecorder(TBaseRecorder tBaseRecorder) {
@@ -99,5 +102,6 @@ public boolean handleSend(TBase<?, ?> data) {
             ResettableServerMetaDataHolder resettableServerMetaDataHolder = (ResettableServerMetaDataHolder) serverMetaDataHolder;
             this.setServerMetaDataHolder(resettableServerMetaDataHolder);
         }
+        this.serverMetaDataHolder.addListener(this.listener);
     }
 }
diff --git a/profiler/src/test/java/com/navercorp/pinpoint/profiler/AgentInfoSenderTest.java b/profiler/src/test/java/com/navercorp/pinpoint/profiler/AgentInfoSenderTest.java
index 72a5abd44e9e..3f0fbaac526a 100644
--- a/profiler/src/test/java/com/navercorp/pinpoint/profiler/AgentInfoSenderTest.java
+++ b/profiler/src/test/java/com/navercorp/pinpoint/profiler/AgentInfoSenderTest.java
@@ -19,18 +19,30 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import java.util.ArrayList;
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
+import java.util.Queue;
 import java.util.Random;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import com.navercorp.pinpoint.bootstrap.context.ServerMetaData;
+import com.navercorp.pinpoint.bootstrap.context.ServerMetaDataHolder;
+import com.navercorp.pinpoint.bootstrap.context.ServiceInfo;
 import com.navercorp.pinpoint.common.ServiceType;
+
 import org.apache.thrift.TException;
 import org.junit.Test;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.navercorp.pinpoint.bootstrap.context.ServerMetaDataHolder;
+import com.navercorp.pinpoint.profiler.context.DefaultServerMetaData;
 import com.navercorp.pinpoint.profiler.context.DefaultServerMetaDataHolder;
 import com.navercorp.pinpoint.profiler.sender.TcpDataSender;
 import com.navercorp.pinpoint.rpc.PinpointSocketException;
@@ -68,7 +80,7 @@ public void agentInfoShouldBeSent() throws InterruptedException {
         PinpointSocket socket = createPinpointSocket(HOST, PORT, socketFactory);
 
         TcpDataSender sender = new TcpDataSender(socket);
-        AgentInfoSender agentInfoSender = new AgentInfoSender(sender, agentInfoSendRetryIntervalMs, getAgentInfo(), getServerMetaDataHolder());
+        AgentInfoSender agentInfoSender = new AgentInfoSender(sender, agentInfoSendRetryIntervalMs, getAgentInfo());
 
         try {
             agentInfoSender.start();
@@ -95,7 +107,7 @@ public void agentInfoShouldRetryUntilSuccess() throws InterruptedException {
         PinpointSocket socket = createPinpointSocket(HOST, PORT, socketFactory);
 
         TcpDataSender dataSender = new TcpDataSender(socket);
-        AgentInfoSender agentInfoSender = new AgentInfoSender(dataSender, agentInfoSendRetryIntervalMs, getAgentInfo(), getServerMetaDataHolder());
+        AgentInfoSender agentInfoSender = new AgentInfoSender(dataSender, agentInfoSendRetryIntervalMs, getAgentInfo());
 
         try {
             agentInfoSender.start();
@@ -119,7 +131,7 @@ public void agentInfoShouldBeSentOnlyOnceEvenAfterReconnect() throws Interrupted
         PinpointSocket socket = createPinpointSocket(HOST, PORT, socketFactory);
 
         TcpDataSender dataSender = new TcpDataSender(socket);
-        AgentInfoSender agentInfoSender = new AgentInfoSender(dataSender, agentInfoSendRetryIntervalMs, getAgentInfo(), getServerMetaDataHolder());
+        AgentInfoSender agentInfoSender = new AgentInfoSender(dataSender, agentInfoSendRetryIntervalMs, getAgentInfo());
 
         try {
             agentInfoSender.start();
@@ -150,7 +162,7 @@ public void agentInfoShouldKeepRetrying() throws InterruptedException {
         PinpointSocket socket = createPinpointSocket(HOST, PORT, socketFactory);
 
         TcpDataSender dataSender = new TcpDataSender(socket);
-        AgentInfoSender agentInfoSender = new AgentInfoSender(dataSender, agentInfoSendRetryIntervalMs, getAgentInfo(), getServerMetaDataHolder());
+        AgentInfoSender agentInfoSender = new AgentInfoSender(dataSender, agentInfoSendRetryIntervalMs, getAgentInfo());
 
         try {
             agentInfoSender.start();
@@ -161,6 +173,99 @@ public void agentInfoShouldKeepRetrying() throws InterruptedException {
         assertTrue(requestCount.get() >= minimumAgentInfoSendRetryCount);
         assertEquals(0, successCount.get());
     }
+    
+    @Test
+    public void serverMetaDataShouldBeSentOnPublish() throws InterruptedException {
+        // Given
+        final AtomicInteger requestCount = new AtomicInteger();
+        final AtomicInteger successCount = new AtomicInteger();
+        final long agentInfoSendRetryIntervalMs = 1000L;
+
+        ResponseServerMessageListener serverListener = new ResponseServerMessageListener(requestCount, successCount);
+
+        PinpointServerAcceptor serverAcceptor = createServerAcceptor(serverListener);
+
+        PinpointSocketFactory socketFactory = createPinpointSocketFactory();
+        PinpointSocket socket = createPinpointSocket(HOST, PORT, socketFactory);
+
+        TcpDataSender sender = new TcpDataSender(socket);
+        AgentInfoSender agentInfoSender = new AgentInfoSender(sender, agentInfoSendRetryIntervalMs, getAgentInfo());
+        final List<ServerMetaData> serverMetaDataObjects = new ArrayList<ServerMetaData>();
+        serverMetaDataObjects.add(new DefaultServerMetaData("server1", Collections.<String>emptyList(), Collections.<Integer, String>emptyMap(), Collections.<ServiceInfo>emptyList()));
+        serverMetaDataObjects.add(new DefaultServerMetaData("server2", Collections.<String>emptyList(), Collections.<Integer, String>emptyMap(), Collections.<ServiceInfo>emptyList()));
+        serverMetaDataObjects.add(new DefaultServerMetaData("server3", Collections.<String>emptyList(), Collections.<Integer, String>emptyMap(), Collections.<ServiceInfo>emptyList()));
+        serverMetaDataObjects.add(new DefaultServerMetaData("server4", Collections.<String>emptyList(), Collections.<Integer, String>emptyMap(), Collections.<ServiceInfo>emptyList()));
+        serverMetaDataObjects.add(new DefaultServerMetaData("server5", Collections.<String>emptyList(), Collections.<Integer, String>emptyMap(), Collections.<ServiceInfo>emptyList()));
+        // When
+        try {
+            for (ServerMetaData serverMetaData : serverMetaDataObjects) {
+                agentInfoSender.publishServerMetaData(serverMetaData);
+            }
+            Thread.sleep(10000L);
+        } finally {
+            closeAll(serverAcceptor, agentInfoSender, socket, socketFactory);
+        }
+        // Then
+        assertEquals(5, requestCount.get());
+        assertEquals(5, successCount.get());
+    }
+    
+    @Test
+    public void serverMetaDataCouldBePublishedFromMultipleThreads() throws InterruptedException {
+        // Given
+        final AtomicInteger requestCount = new AtomicInteger();
+        final AtomicInteger successCount = new AtomicInteger();
+        final long agentInfoSendRetryIntervalMs = 1000L;
+        final int threadCount = 50;
+        final CountDownLatch initLatch = new CountDownLatch(threadCount);
+        final CountDownLatch startLatch = new CountDownLatch(1);
+        final CountDownLatch endLatch = new CountDownLatch(threadCount);
+        final ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
+        final Queue<Throwable> exceptions = new ConcurrentLinkedQueue<Throwable>();
+
+        ResponseServerMessageListener delayedServerListener = new ResponseServerMessageListener(requestCount, successCount);
+
+        PinpointServerAcceptor serverAcceptor = createServerAcceptor(delayedServerListener);
+
+        PinpointSocketFactory socketFactory = createPinpointSocketFactory();
+        PinpointSocket socket = createPinpointSocket(HOST, PORT, socketFactory);
+
+        TcpDataSender sender = new TcpDataSender(socket);
+        AgentInfoSender agentInfoSender = new AgentInfoSender(sender, agentInfoSendRetryIntervalMs, getAgentInfo());
+        final ServerMetaDataHolder metaDataContext = new DefaultServerMetaDataHolder(Collections.<String>emptyList());
+        metaDataContext.addListener(agentInfoSender);
+        // When
+        for (int i = 0; i < threadCount; ++i) {
+            executorService.submit(new Callable<Void>() {
+                @Override
+                public Void call() throws Exception {
+                    initLatch.countDown();
+                    try {
+                        startLatch.await();
+                        metaDataContext.publishServerMetaData();
+                    } catch (final Throwable t) {
+                        exceptions.add(t);
+                    } finally {
+                        endLatch.countDown();
+                    }
+                    return null;
+                }
+            });
+        }
+        initLatch.await();
+        startLatch.countDown();
+        endLatch.await();
+        executorService.shutdown();
+        try {
+            Thread.sleep(10000L);
+        } finally {
+            closeAll(serverAcceptor, agentInfoSender, socket, socketFactory);
+        }
+        // Then
+        assertTrue("Failed with exceptions : " + exceptions, exceptions.isEmpty());
+        assertEquals(threadCount, requestCount.get());
+        assertEquals(threadCount, successCount.get());
+    }
 
     public void reconnectionStressTest() throws InterruptedException {
         final AtomicInteger requestCount = new AtomicInteger();
@@ -176,7 +281,7 @@ public void reconnectionStressTest() throws InterruptedException {
         PinpointSocket socket = createPinpointSocket(HOST, PORT, socketFactory);
 
         TcpDataSender dataSender = new TcpDataSender(socket);
-        AgentInfoSender agentInfoSender = new AgentInfoSender(dataSender, agentInfoSendRetryIntervalMs, getAgentInfo(), getServerMetaDataHolder());
+        AgentInfoSender agentInfoSender = new AgentInfoSender(dataSender, agentInfoSendRetryIntervalMs, getAgentInfo());
 
         long startTime = System.currentTimeMillis();
 
@@ -242,11 +347,6 @@ private AgentInformation getAgentInfo() {
         return agentInfo;
     }
 
-    private ServerMetaDataHolder getServerMetaDataHolder() {
-        ServerMetaDataHolder serverMetaDataHolder = new DefaultServerMetaDataHolder(Collections.<String>emptyList());
-        return serverMetaDataHolder;
-    }
-
     class ResponseServerMessageListener implements ServerMessageListener {
         private final AtomicInteger requestCount;
         private final AtomicInteger successCount;
@@ -295,7 +395,7 @@ public void handleRequest(RequestPacket requestPacket, PinpointServer pinpointSe
         }
 
         @Override
-        public HandshakeResponseCode handleHandshake(Map arg0) {
+        public HandshakeResponseCode handleHandshake(@SuppressWarnings("rawtypes") Map arg0) {
             return HandshakeResponseType.Success.DUPLEX_COMMUNICATION;
         }
     }
diff --git a/profiler/src/test/java/com/navercorp/pinpoint/profiler/context/DefaultServerMetaDataHolderTest.java b/profiler/src/test/java/com/navercorp/pinpoint/profiler/context/DefaultServerMetaDataHolderTest.java
index c818c8eea016..2d51b4582380 100644
--- a/profiler/src/test/java/com/navercorp/pinpoint/profiler/context/DefaultServerMetaDataHolderTest.java
+++ b/profiler/src/test/java/com/navercorp/pinpoint/profiler/context/DefaultServerMetaDataHolderTest.java
@@ -36,6 +36,7 @@
 import com.navercorp.pinpoint.bootstrap.context.ServerMetaData;
 import com.navercorp.pinpoint.bootstrap.context.ServerMetaDataHolder;
 import com.navercorp.pinpoint.profiler.context.DefaultServerMetaDataHolder;
+import com.navercorp.pinpoint.test.TestableServerMetaDataListener;
 
 /**
  * @author hyungil.jeong
@@ -48,7 +49,7 @@ public class DefaultServerMetaDataHolderTest {
     private static final List<String> VM_ARGS = Arrays.asList("testVmArgs");
 
     private ExecutorService executorService;
-
+    
     @Before
     public void setUp() {
         this.executorService = Executors.newFixedThreadPool(THREAD_COUNT);
@@ -68,7 +69,9 @@ public void testRaceConditionWhenAddingServiceInfo() throws InterruptedException
         final Queue<Throwable> exceptions = new ConcurrentLinkedQueue<Throwable>();
         
         final String serviceName = "/test";
+        final TestableServerMetaDataListener listener = new TestableServerMetaDataListener();
         final ServerMetaDataHolder metaDataContext = new DefaultServerMetaDataHolder(VM_ARGS);
+        metaDataContext.addListener(listener);
         metaDataContext.setServerName(SERVER_INFO);
         // When
         for (int i = 0; i < THREAD_COUNT; ++i) {
@@ -80,6 +83,7 @@ public Void call() throws Exception {
                     try {
                         startLatch.await();
                         metaDataContext.addServiceInfo(serviceName, serviceLibs);
+                        metaDataContext.publishServerMetaData();
                     } catch (final Throwable t) {
                         exceptions.add(t);
                     } finally {
@@ -94,7 +98,7 @@ public Void call() throws Exception {
         endLatch.await();
         // Then
         assertTrue("Failed with exceptions : " + exceptions, exceptions.isEmpty());
-        ServerMetaData metaData = metaDataContext.getServerMetaData();
+        ServerMetaData metaData = listener.getServerMetaData();
         assertEquals(metaData.getServerInfo(), SERVER_INFO);
         assertEquals(metaData.getVmArgs(), VM_ARGS);
         assertEquals(metaData.getServiceInfos().size(), THREAD_COUNT);
@@ -108,7 +112,9 @@ public void testRaceConditionWhenAddingAndInteratingServiceInfo() throws Interru
         final CountDownLatch endLatch = new CountDownLatch(THREAD_COUNT);
         final Queue<Throwable> exceptions = new ConcurrentLinkedQueue<Throwable>();
         
+        final TestableServerMetaDataListener listener = new TestableServerMetaDataListener();
         final ServerMetaDataHolder metaDataContext = new DefaultServerMetaDataHolder(VM_ARGS);
+        metaDataContext.addListener(listener);
         metaDataContext.setServerName(SERVER_INFO);
         // When
         final List<ServerMetaData> serverMetaDatas = new Vector<ServerMetaData>(THREAD_COUNT/2);
@@ -123,6 +129,7 @@ public Void call() throws Exception {
                         try {
                             startLatch.await();
                             metaDataContext.addServiceInfo(serviceName, serviceLibs);
+                            metaDataContext.publishServerMetaData();
                         } catch (Throwable t) {
                             exceptions.add(t);
                         } finally {
@@ -138,7 +145,7 @@ public Void call() throws Exception {
                         initLatch.countDown();
                         try {
                             startLatch.await();
-                            ServerMetaData serverMetaData = metaDataContext.getServerMetaData();
+                            ServerMetaData serverMetaData = listener.getServerMetaData();
                             serverMetaDatas.add(serverMetaData);
                         } catch (Throwable t) {
                             exceptions.add(t);
@@ -155,11 +162,11 @@ public Void call() throws Exception {
         endLatch.await();
         // Then
         assertTrue("Failed with exceptions : " + exceptions, exceptions.isEmpty());
-        ServerMetaData metaData = metaDataContext.getServerMetaData();
+        ServerMetaData metaData = listener.getServerMetaData();
         assertEquals(metaData.getServerInfo(), SERVER_INFO);
         assertEquals(metaData.getVmArgs(), VM_ARGS);
         assertEquals(metaData.getServiceInfos().size(), THREAD_COUNT/2);
         assertEquals(serverMetaDatas.size(), THREAD_COUNT/2);
     }
-
+    
 }