package com.ctrip.framework.apollo.configservice.controller; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.HashMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import com.ctrip.framework.apollo.biz.entity.ReleaseMessage; import com.ctrip.framework.apollo.biz.message.ReleaseMessageListener; import com.ctrip.framework.apollo.biz.message.Topics; import com.ctrip.framework.apollo.biz.utils.EntityManagerUtil; import com.ctrip.framework.apollo.configservice.service.ReleaseMessageServiceWithCache; import com.ctrip.framework.apollo.configservice.util.NamespaceUtil; import com.ctrip.framework.apollo.configservice.util.WatchKeysUtil; import com.ctrip.framework.apollo.core.ConfigConsts; import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification; import com.ctrip.framework.apollo.tracer.Tracer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import java.util.List; import java.util.Set; /** * @author Jason Song(song_s@ctrip.com) */ @RestController @RequestMapping("/notifications") public class NotificationController implements ReleaseMessageListener { private static final Logger logger = LoggerFactory.getLogger(NotificationController.class); private static final long TIMEOUT = 30 * 1000;//30 seconds private final Multimap<String, DeferredResult<ResponseEntity<ApolloConfigNotification>>> deferredResults = Multimaps.synchronizedSetMultimap(HashMultimap.create()); private static final ResponseEntity<ApolloConfigNotification> NOT_MODIFIED_RESPONSE = new ResponseEntity<>(HttpStatus.NOT_MODIFIED); private static final Splitter STRING_SPLITTER = Splitter.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).omitEmptyStrings(); @Autowired private WatchKeysUtil watchKeysUtil; @Autowired private ReleaseMessageServiceWithCache releaseMessageService; @Autowired private EntityManagerUtil entityManagerUtil; @Autowired private NamespaceUtil namespaceUtil; /** * For single namespace notification, reserved for older version of apollo clients * * @param appId the appId * @param cluster the cluster * @param namespace the namespace name * @param dataCenter the datacenter * @param notificationId the notification id for the namespace * @param clientIp the client side ip * @return a deferred result */ @RequestMapping(method = RequestMethod.GET) public DeferredResult<ResponseEntity<ApolloConfigNotification>> pollNotification( @RequestParam(value = "appId") String appId, @RequestParam(value = "cluster") String cluster, @RequestParam(value = "namespace", defaultValue = ConfigConsts.NAMESPACE_APPLICATION) String namespace, @RequestParam(value = "dataCenter", required = false) String dataCenter, @RequestParam(value = "notificationId", defaultValue = "-1") long notificationId, @RequestParam(value = "ip", required = false) String clientIp) { //strip out .properties suffix namespace = namespaceUtil.filterNamespaceName(namespace); Set<String> watchedKeys = watchKeysUtil.assembleAllWatchKeys(appId, cluster, namespace, dataCenter); DeferredResult<ResponseEntity<ApolloConfigNotification>> deferredResult = new DeferredResult<>(TIMEOUT, NOT_MODIFIED_RESPONSE); //check whether client is out-dated ReleaseMessage latest = releaseMessageService.findLatestReleaseMessageForMessages(watchedKeys); /** * Manually close the entity manager. * Since for async request, Spring won't do so until the request is finished, * which is unacceptable since we are doing long polling - means the db connection would be hold * for a very long time */ entityManagerUtil.closeEntityManager(); if (latest != null && latest.getId() != notificationId) { deferredResult.setResult(new ResponseEntity<>( new ApolloConfigNotification(namespace, latest.getId()), HttpStatus.OK)); } else { //register all keys for (String key : watchedKeys) { this.deferredResults.put(key, deferredResult); } deferredResult .onTimeout(() -> logWatchedKeysToCat(watchedKeys, "Apollo.LongPoll.TimeOutKeys")); deferredResult.onCompletion(() -> { //unregister all keys for (String key : watchedKeys) { deferredResults.remove(key, deferredResult); } logWatchedKeysToCat(watchedKeys, "Apollo.LongPoll.CompletedKeys"); }); logWatchedKeysToCat(watchedKeys, "Apollo.LongPoll.RegisteredKeys"); logger.debug("Listening {} from appId: {}, cluster: {}, namespace: {}, datacenter: {}", watchedKeys, appId, cluster, namespace, dataCenter); } return deferredResult; } @Override public void handleMessage(ReleaseMessage message, String channel) { logger.info("message received - channel: {}, message: {}", channel, message); String content = message.getMessage(); Tracer.logEvent("Apollo.LongPoll.Messages", content); if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty(content)) { return; } List<String> keys = STRING_SPLITTER.splitToList(content); //message should be appId+cluster+namespace if (keys.size() != 3) { logger.error("message format invalid - {}", content); return; } ResponseEntity<ApolloConfigNotification> notification = new ResponseEntity<>( new ApolloConfigNotification(keys.get(2), message.getId()), HttpStatus.OK); if (!deferredResults.containsKey(content)) { return; } //create a new list to avoid ConcurrentModificationException List<DeferredResult<ResponseEntity<ApolloConfigNotification>>> results = Lists.newArrayList(deferredResults.get(content)); logger.debug("Notify {} clients for key {}", results.size(), content); for (DeferredResult<ResponseEntity<ApolloConfigNotification>> result : results) { result.setResult(notification); } logger.debug("Notification completed"); } private void logWatchedKeysToCat(Set<String> watchedKeys, String eventName) { for (String watchedKey : watchedKeys) { Tracer.logEvent(eventName, watchedKey); } } }