/* * 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.web.websocket; import com.fasterxml.jackson.core.JsonProcessingException; import com.navercorp.pinpoint.common.server.util.AgentLifeCycleState; import com.navercorp.pinpoint.web.service.AgentService; import com.navercorp.pinpoint.web.vo.AgentActiveThreadCount; import com.navercorp.pinpoint.web.vo.AgentActiveThreadCountList; import com.navercorp.pinpoint.web.vo.AgentInfo; import com.navercorp.pinpoint.web.vo.AgentStatus; import com.navercorp.pinpoint.web.websocket.message.PinpointWebSocketMessageConverter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Timer; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicInteger; /** * @Author Taejin Koo */ public class ActiveThreadCountResponseAggregator implements PinpointWebSocketResponseAggregator { private static final String APPLICATION_NAME = "applicationName"; private static final String ACTIVE_THREAD_COUNTS = "activeThreadCounts"; private static final String TIME_STAMP = "timeStamp"; private final static int LOG_RECORD_RATE = 60; private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final String applicationName; private final AgentService agentService; private final Timer timer; private final Object workerManagingLock = new Object(); private final List<WebSocketSession> webSocketSessions = new CopyOnWriteArrayList<>(); private final ConcurrentMap<String, ActiveThreadCountWorker> activeThreadCountWorkerRepository = new ConcurrentHashMap<>(); private final Object aggregatorLock = new Object(); private final PinpointWebSocketMessageConverter messageConverter; private final AtomicInteger flushCount = new AtomicInteger(0); private volatile boolean isStopped = false; private WorkerActiveManager workerActiveManager; private Map<String, AgentActiveThreadCount> activeThreadCountMap = new HashMap<>(); public ActiveThreadCountResponseAggregator(String applicationName, AgentService agentService, Timer timer) { this.applicationName = applicationName; this.agentService = agentService; this.timer = timer; this.messageConverter = new PinpointWebSocketMessageConverter(); } @Override public void start() { synchronized (workerManagingLock) { workerActiveManager = new WorkerActiveManager(this, agentService, timer); } } @Override public void stop() { synchronized (workerManagingLock) { isStopped = true; if (workerActiveManager != null) { this.workerActiveManager.close(); } for (ActiveThreadCountWorker worker : activeThreadCountWorkerRepository.values()) { if (worker != null) { worker.stop(); } } activeThreadCountWorkerRepository.clear(); } } @Override public void addWebSocketSession(WebSocketSession webSocketSession) { if (webSocketSession == null) { return; } logger.info("addWebSocketSession. applicationName:{}, webSocketSession:{}", applicationName, webSocketSession); List<AgentInfo> agentInfoList = agentService.getRecentAgentInfoList(applicationName); synchronized (workerManagingLock) { if (isStopped) { return; } for (AgentInfo agentInfo : agentInfoList) { AgentStatus agentStatus = agentInfo.getStatus(); if (agentStatus != null && agentStatus.getState() != AgentLifeCycleState.UNKNOWN) { activeWorker(agentInfo); } else if (agentService.isConnected(agentInfo)) { activeWorker(agentInfo); } } boolean added = webSocketSessions.add(webSocketSession); if (added && webSocketSessions.size() == 1) { workerActiveManager.startAgentCheckJob(); } } } // return when aggregator cleared. @Override public boolean removeWebSocketSessionAndGetIsCleared(WebSocketSession webSocketSession) { if (webSocketSession == null) { return false; } logger.info("removeWebSocketSessionAndGetIsCleared. applicationName{}, webSocketSession:{}", applicationName, webSocketSession); synchronized (workerManagingLock) { if (isStopped) { return true; } boolean removed = webSocketSessions.remove(webSocketSession); if (removed && webSocketSessions.isEmpty()) { for (ActiveThreadCountWorker activeThreadCountWorker : activeThreadCountWorkerRepository.values()) { activeThreadCountWorker.stop(); } activeThreadCountWorkerRepository.clear(); return true; } } return false; } @Override public void addActiveWorker(AgentInfo agentInfo) { logger.info("activeWorker applicationName:{}, agentId:{}", applicationName, agentInfo.getAgentId()); if (!applicationName.equals(agentInfo.getApplicationName())) { return; } synchronized (workerManagingLock) { if (isStopped) { return; } activeWorker(agentInfo); } } private void activeWorker(AgentInfo agentInfo) { String agentId = agentInfo.getAgentId(); synchronized (workerManagingLock) { ActiveThreadCountWorker worker = activeThreadCountWorkerRepository.get(agentId); if (worker == null) { worker = new ActiveThreadCountWorker(agentService, agentInfo, this, workerActiveManager); worker.start(agentInfo); activeThreadCountWorkerRepository.put(agentId, worker); } else { worker.reactive(agentInfo); } } } @Override public void response(AgentActiveThreadCount activeThreadCount) { if (activeThreadCount == null) { return; } synchronized (aggregatorLock) { this.activeThreadCountMap.put(activeThreadCount.getAgentId(), activeThreadCount); } } @Override public void flush() throws Exception { flush(null); } @Override public void flush(Executor executor) throws Exception { if ((flushCount.getAndIncrement() % LOG_RECORD_RATE) == 0) { logger.info("flush started. applicationName:{}", applicationName); } if (isStopped) { return; } AgentActiveThreadCountList response = new AgentActiveThreadCountList(); synchronized (aggregatorLock) { for (ActiveThreadCountWorker activeThreadCountWorker : activeThreadCountWorkerRepository.values()) { String agentId = activeThreadCountWorker.getAgentId(); AgentActiveThreadCount agentActiveThreadCount = activeThreadCountMap.get(agentId); if (agentActiveThreadCount != null) { response.add(agentActiveThreadCount); } else { response.add(activeThreadCountWorker.getDefaultFailResponse()); } } activeThreadCountMap = new HashMap<>(activeThreadCountWorkerRepository.size()); } TextMessage webSocketTextMessage = createWebSocketTextMessage(response); if (webSocketTextMessage != null) { if (executor == null) { flush0(webSocketTextMessage); } else { flushAsync0(webSocketTextMessage, executor); } } } private TextMessage createWebSocketTextMessage(AgentActiveThreadCountList activeThreadCountList) { Map resultMap = createResultMap(activeThreadCountList, System.currentTimeMillis()); try { TextMessage responseTextMessage = new TextMessage(messageConverter.getResponseTextMessage(ActiveThreadCountHandler.API_ACTIVE_THREAD_COUNT, resultMap)); return responseTextMessage; } catch (JsonProcessingException e) { logger.warn("failed while to convert message. applicationName:{}, original:{}, message:{}.", applicationName, resultMap, e.getMessage(), e); } return null; } private void flush0(TextMessage webSocketMessage) { for (WebSocketSession webSocketSession : webSocketSessions) { try { logger.debug("flush webSocketSession:{}, response:{}", webSocketSession, webSocketMessage); webSocketSession.sendMessage(webSocketMessage); } catch (Exception e) { logger.warn("failed while flushing message to webSocket. session:{}, message:{}, error:{}", webSocketSession, webSocketMessage, e.getMessage(), e); } } } private void flushAsync0(TextMessage webSocketMessage, Executor executor) { for (WebSocketSession webSocketSession : webSocketSessions) { if (webSocketSession == null) { logger.warn("failed caused webSocketSession is null. applicationName:{}", applicationName); continue; } executor.execute(new OrderedWebSocketFlushRunnable(webSocketSession, webSocketMessage)); } } @Override public String getApplicationName() { return applicationName; } private Map createResultMap(AgentActiveThreadCountList activeThreadCount, long timeStamp) { Map<String, Object> response = new HashMap<>(); response.put(APPLICATION_NAME, applicationName); response.put(ACTIVE_THREAD_COUNTS, activeThreadCount); response.put(TIME_STAMP, timeStamp); return response; } }