How Nacos Implements Long‑Polling: Deep Dive into Client & Server Mechanics
This article explains the inner workings of Nacos' long‑polling mechanism, covering both client‑side scheduling and server‑side handling, with detailed code walkthroughs, architectural diagrams, and insights into how configuration changes are detected and propagated in a distributed system.
This article introduces one of the core principles of the Nacos configuration center: the application of the long‑polling mechanism, providing useful insights for developers.
For clarity, the Nacos console and registration center are referred to as the Nacos server, while the business services we develop are called Nacos clients.
1. Client long‑polling scheduling mechanism
We start from NacosPropertySourceLocator.locate() and step into the source code.
1.1 Instantiating NacosConfigService via reflection
The client’s long‑polling task is started in NacosFactory.createConfigService() when constructing the ConfigService instance.
public static ConfigService createConfigService(Properties properties) throws NacosException {
//【Breakpoint】create ConfigService
return ConfigFactory.createConfigService(properties);
}Entering ConfigFactory.createConfigService() reveals the use of reflection to instantiate a NacosConfigService object.
1.2 NacosConfigService constructor starts the long‑polling task
Inside NacosConfigService.NacosConfigService() the constructor sets several properties related to remote tasks.
1.2.1 Initializing HttpAgent
The design of MetricsHttpAgent and ServerHttpAgent classes is shown below.
1.2.2 Initializing ClientWorker
The ClientWorker.ClientWorker() constructor creates two scheduled thread pools and starts a periodic task.
The ClientWorker.checkConfigInfo() method checks for configuration changes every 10 seconds.
cacheMap : an AtomicReference<Map<String, CacheData>> storing listeners keyed by dataId/group/tenant.
Long‑polling task splitting : by default each LongPollingRunnable handles 3000 listeners; more listeners trigger additional runnables.
1.3 Checking configuration changes – LongPollingRunnable.run()
The method iterates over cached data, checks local files, and if changes are detected, fetches the updated configuration from the server.
public void run() {
List<CacheData> cacheDatas = new ArrayList();
ArrayList inInitializingCacheList = new ArrayList();
try {
// Iterate CacheData and check local config
Iterator var3 = ((Map)ClientWorker.this.cacheMap.get()).values().iterator();
while (var3.hasNext()) {
CacheData cacheData = (CacheData) var3.next();
if (cacheData.getTaskId() == this.taskId) {
cacheDatas.add(cacheData);
try {
// Check local config
ClientWorker.this.checkLocalConfig(cacheData);
if (cacheData.isUseLocalConfigInfo()) {
cacheData.checkListenerMd5();
}
} catch (Exception var13) {
ClientWorker.LOGGER.error("get local config info error", var13);
}
}
}
//【Breakpoint 1.3.1】Long‑polling request to check server config changes
List<String> changedGroupKeys = ClientWorker.this.checkUpdateDataIds(cacheDatas, inInitializingCacheList);
// Reload changed configs
Iterator var16 = changedGroupKeys.iterator();
while (var16.hasNext()) {
String groupKey = (String) var16.next();
String[] key = GroupKey.parseKey(groupKey);
String dataId = key[0];
String group = key[1];
String tenant = null;
if (key.length == 3) {
tenant = key[2];
}
try {
//【Breakpoint 1.3.2】Read changed config
String content = ClientWorker.this.getServerConfig(dataId, group, tenant, 3000L);
CacheData cache = (CacheData) ((Map) ClientWorker.this.cacheMap.get()).get(GroupKey.getKeyTenant(dataId, group, tenant));
cache.setContent(content);
ClientWorker.LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}", new Object[]{ClientWorker.this.agent.getName(), dataId, group, tenant, cache.getMd5(), ContentUtils.truncateContent(content)});
} catch (NacosException var12) {
String message = String.format("[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s", ClientWorker.this.agent.getName(), dataId, group, tenant);
ClientWorker.LOGGER.error(message, var12);
}
}
// Trigger event notifications
var16 = cacheDatas.iterator();
while (true) {
CacheData cacheDatax;
do {
if (!var16.hasNext()) {
inInitializingCacheList.clear();
// Continue periodic execution
ClientWorker.this.executorService.execute(this);
return;
}
cacheDatax = (CacheData) var16.next();
} while (cacheDatax.isInitializing() && !inInitializingCacheList.contains(GroupKey.getKeyTenant(cacheDatax.dataId, cacheDatax.group, cacheDatax.tenant)));
cacheDatax.checkListenerMd5();
cacheDatax.setInitializing(false);
}
} catch (Throwable var14) {
ClientWorker.LOGGER.error("longPolling error : ", var14);
ClientWorker.this.executorService.schedule(this, (long) ClientWorker.this.taskPenaltyTime, TimeUnit.MILLISECONDS);
}
}Note: To observe the breakpoint, modify the configuration on the Nacos server with an interval greater than 30 seconds.
1.3.1 Checking updates – ClientWorker.checkUpdateDataIds()
This method ultimately calls ClientWorker.checkUpdateConfigStr(), which sends a long‑polling request to /v1/cs/configs/listener via MetricsHttpAgent.httpPost().
The request sets a long timeout (default 30 s).
If the server detects changes, it returns a response containing the changed dataId, group, and tenant.
The client then reads the new configuration using ClientWorker.getServerConfig().
List<String> checkUpdateConfigStr(String probeUpdateString, boolean isInitializingCacheList) throws IOException {
List<String> params = Arrays.asList("Listening-Configs", probeUpdateString);
List<String> headers = new ArrayList(2);
headers.add("Long-Pulling-Timeout");
headers.add("" + this.timeout);
if (isInitializingCacheList) {
headers.add("Long-Pulling-Timeout-No-Hangup");
headers.add("true");
}
if (StringUtils.isBlank(probeUpdateString)) {
return Collections.emptyList();
} else {
try {
// Call /v1/cs/configs/listener
HttpResult result = this.agent.httpPost("/v1/cs/configs/listener", headers, params, this.agent.getEncode(), this.timeout);
if (200 == result.code) {
this.setHealthServer(true);
return this.parseUpdateDataIdResponse(result.content);
}
this.setHealthServer(false);
LOGGER.error("[{}] [check-update] get changed dataId error, code: {}", this.agent.getName(), result.code);
} catch (IOException var6) {
this.setHealthServer(false);
LOGGER.error("[" + this.agent.getName() + "] [check-update] get changed dataId exception", var6);
throw var6;
}
return Collections.emptyList();
}
}1.3.2 Reading changed config – ClientWorker.getServerConfig()
This method uses MetricsHttpAgent.httpGet() to call /v1/cs/configs, retrieves the configuration content, and saves a snapshot locally via LocalConfigInfoProcessor.saveSnapshot().
2. Server‑side long‑polling scheduling mechanism
2.1 Server receives request – ConfigController.listener()
The Nacos server processes client requests through the /listener endpoint defined in ConfigController.
2.2 Executing the long‑polling request – ConfigServletInner.doPollingConfig()
This method encapsulates the long‑polling logic and also supports short‑polling.
2.3 Creating the scheduled task – ClientLongPolling.run()
The server holds the request for up to 30 seconds; if no change occurs, it returns after a short delay (≈0.5 s).
This keeps the client‑server connection alive while configurations remain unchanged.
2.4 Listening to configuration change events
2.4.1 Handling LocalDataChangeEvent
When a configuration is modified on the Nacos server (or via API), a LocalDataChangeEvent is published and observed by LongPollingService.
Before Nacos 1.3.1, LongPollingService extended AbstractEventListener and overrode onEvent(). After 1.3.1, a Subscriber handles the event via onEvent().
2.4.2 Event processing – DataChangeTask.run()
The DataChangeTask runs in a thread pool, retrieves the updated configuration based on groupKey, and pushes the change back to the client.
3. Source code structure summary
3.1 Client‑side long‑polling flow
NacosPropertySourceLocator.locate()– initializes ConfigService and locates configuration. NacosConfigService.NacosConfigService() – constructor. Executors.newScheduledThreadPool() – creates executor pools. ClientWorker.checkConfigInfo() – periodic check for changes. ClientWorker.checkLocalConfig() – validates local files. ClientWorker.checkUpdateDataIds() – queries server for updates. ClientWorker.getServerConfig() – fetches changed config. MetricsHttpAgent.httpPost() – calls /v1/cs/configs/listener for long‑polling. MetricsHttpAgent.httpGet() – calls /v1/cs/configs to retrieve config. LongPollingRunnable.run() – runs the scheduled long‑polling thread.
3.2 Server‑side long‑polling flow
ConfigController.listener()– receives client request. LongPollingService.addLongPollingClient() – core handling, returns early after 500 ms. ClientLongPolling.run() – implements the server‑side timing logic. Map.put() / Queue.remove() – manage polling client collections. MD5Util.compareMd5() – compare configuration hashes. LongPollingService.sendResponse() – returns change results to client. ConfigExecutor.scheduleLongPolling() – schedules task with ~29.5 s delay.
3.3 Server‑side event listening
When a configuration changes, a LocalDataChangeEvent is emitted. Subscriber.onEvent() (post‑1.3.2) listens to the event. DataChangeTask.run() retrieves the updated data and triggers long‑polling response.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java High-Performance Architecture
Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
