Deep Dive into Nacos Long‑Polling Mechanism: Client and Server Implementation Details

This article provides a comprehensive analysis of Nacos's long‑polling mechanism, walking through the client‑side ConfigService instantiation, scheduled thread pools, request handling, and the server‑side listener workflow, while highlighting key code snippets and the event‑driven architecture that enables real‑time configuration updates.

Top Architect
Top Architect
Top Architect
Deep Dive into Nacos Long‑Polling Mechanism: Client and Server Implementation Details

For clarity, the Nacos console and registration center are referred to as the Nacos server (the web UI), while the business services we develop are called Nacos clients.

The source‑code analysis is split into two parts; this part focuses on the long‑polling timing mechanism on both client and server sides.

1. Client Long‑Polling Timing Mechanism

Start from the previous article’s NacosPropertySourceLocator.locate() call.

The NacosFactory.createConfigService() method creates a ConfigService instance, which triggers the long‑polling task.

public static ConfigService createConfigService(Properties properties) throws NacosException {
    //【Breakpoint】Create ConfigService
    return ConfigFactory.createConfigService(properties);
}

Inside ConfigFactory.createConfigService(), reflection is used to instantiate NacosConfigService:

public static ConfigService createConfigService(Properties properties) throws NacosException {
    try {
        Class<?> driverImplClass = Class.forName("com.alibaba.nacos.client.config.NacosConfigService");
        Constructor constructor = driverImplClass.getConstructor(Properties.class);
        ConfigService vendorImpl = (ConfigService) constructor.newInstance(properties);
        return vendorImpl;
    } catch (Throwable var4) {
        throw new NacosException(-400, var4);
    }
}

The NacosConfigService constructor launches the long‑polling task:

public NacosConfigService(Properties properties) throws NacosException {
    // Initialize namespace, HttpAgent, and ClientWorker
    this.agent = new MetricsHttpAgent(new ServerHttpAgent(properties));
    this.agent.start();
    this.worker = new ClientWorker(this.agent, this.configFilterChainManager, properties);
}
ClientWorker

creates two scheduled thread pools and starts a periodic task that checks configuration every 10 seconds:

this.executor = Executors.newScheduledThreadPool(1, r -> {
    Thread t = new Thread(r);
    t.setName("com.alibaba.nacos.client.Worker." + agent.getName());
    t.setDaemon(true);
    return t;
});
this.executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), r -> {
    Thread t = new Thread(r);
    t.setName("com.alibaba.nacos.client.Worker.longPolling." + agent.getName());
    t.setDaemon(true);
    return t;
});
this.executor.scheduleWithFixedDelay(() -> {
    try {
        ClientWorker.this.checkConfigInfo();
    } catch (Throwable var2) {
        ClientWorker.LOGGER.error("[" + agent.getName() + "][sub-check] rotate check error", var2);
    }
}, 1L, 10L, TimeUnit.MILLISECONDS);

The checkConfigInfo() method splits the listener set into tasks, creates LongPollingRunnable threads when the listener count exceeds 3000, and invokes checkUpdateDataIds() to query the server for changes.

public void checkConfigInfo() {
    int listenerSize = ((Map) this.cacheMap.get()).size();
    int longingTaskCount = (int) Math.ceil((double) listenerSize / ParamUtil.getPerTaskConfigSize());
    if ((double) longingTaskCount > this.currentLongingTaskCount) {
        for (int i = (int) this.currentLongingTaskCount; i < longingTaskCount; ++i) {
            this.executorService.execute(new ClientWorker.LongPollingRunnable(i));
        }
        this.currentLongingTaskCount = (double) longingTaskCount;
    }
}

When a change is detected, LongPollingRunnable.run() reads the updated configuration via ClientWorker.getServerConfig(), which ultimately calls the server’s /v1/cs/configs endpoint.

public String getServerConfig(String dataId, String group, String tenant, long readTimeout) throws NacosException {
    if (StringUtils.isBlank(group)) {
        group = "DEFAULT_GROUP";
    }
    HttpResult result = this.agent.httpGet("/v1/cs/configs", null, params, this.agent.getEncode(), readTimeout);
    switch (result.code) {
        case 200:
            LocalConfigInfoProcessor.saveSnapshot(this.agent.getName(), dataId, group, tenant, result.content);
            return result.content;
        // other status handling omitted for brevity
    }
}

2. Server Long‑Polling Timing Mechanism

The server receives the client request at ConfigController.listener(), which delegates to LongPollingService.addLongPollingClient() to set up the asynchronous context.

@PostMapping("/listener")
public void listener(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", true);
    String probeModify = request.getParameter("Listening-Configs");
    // MD5 calculation and async handling omitted for brevity
    inner.doPollingConfig(request, response, clientMd5Map, probeModify.length());
}
LongPollingService.addLongPollingClient()

extracts timeout headers, creates an AsyncContext, and schedules a ClientLongPolling task:

final AsyncContext asyncContext = req.startAsync();
asyncContext.setTimeout(0L);
ConfigExecutor.executeLongPolling(new ClientLongPolling(asyncContext, clientMd5Map, ip, probeRequestSize, timeout, appName, tag));

The ClientLongPolling.run() method schedules a delayed task (≈29.5 s). When the timeout expires, it checks for configuration changes; if none, it returns an empty response, otherwise it sends the changed group keys.

asyncTimeoutFuture = ConfigExecutor.scheduleLongPolling(() -> {
    // Remove from subscription list, compare MD5, send response
    if (changedGroups.size() > 0) {
        sendResponse(changedGroups);
    } else {
        sendResponse(null);
    }
}, timeoutTime, TimeUnit.MILLISECONDS);

Server‑side configuration changes fire a LocalDataChangeEvent, which is listened to by LongPollingService (pre‑1.3.1) or a registered Subscriber (post‑1.3.2). The listener creates a DataChangeTask that iterates over pending ClientLongPolling instances and immediately notifies matching clients.

public void onEvent(Event event) {
    if (event instanceof LocalDataChangeEvent) {
        LocalDataChangeEvent evt = (LocalDataChangeEvent) event;
        ConfigExecutor.executeLongPolling(new DataChangeTask(evt.groupKey, evt.isBeta, evt.betaIps));
    }
}

3. Summary of the Source Structure

The client side builds the ConfigService via reflection, creates scheduled executors, periodically checks for updates, and fetches changed configurations through HTTP calls. The server side exposes a /listener endpoint, registers long‑polling clients, schedules timeout tasks, compares MD5 hashes, and pushes change notifications via asynchronous responses.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavamicroservicesNacoslongpollingConfigService
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.