Skip to content

Multi-root workspace: activating each namespace reconnects all active connections and orphans CSP sessions, exhausting the license #1805

Description

@tobikrs

Are you using client-side or server-side editing?:
Client-side only (multi-root workspace: one folder per namespace, each with a folder-level objectscript.conn, plus a project working-directory folder)

  • VS Code Version: 1.127.0
  • Extension Version: 3.8.2
  • OS: Linux (WSL2)
  • Server $ZVERSION:
    • lastly observed: IRIS for UNIX (Ubuntu Server LTS for x86-64 Containers) 2024.1.6 (Build 825U) Tue Mar 24 2026 15:11:45 EDT
    • issue is also present in newer versions of IRIS

Summary

In a multi-root workspace with many namespace folders on the same iris instance, activating connections exhausts the server's CSP session/license limit far faster than the number of namespaces. Any change to objectscript.conn forces every already-active connection to reconnect, and each reconnect orphans its previous CSP session on the server instead of reusing or logging it out. The stranded sessions accumulate until the license limit is hit.

This happens in both of these scenarios:

  • Activating connections one-by-one (edit active: true per folder) — session count grows super-linearly (table below).
  • Activating all connections at once (e.g. git revert/git restore that rewrites every folder's settings.json back to active: true simultaneously) — also exhausts the license.

The "all at once" case is the clearest demonstration of the root cause: a single config-change pass loops over all N active folders sequentially, and because they share one cookie slot (see below), each iteration drops the prior folder's cookie (orphaning that server session) and mints a new one — so one pass alone strands ~N−1 sessions, before accounting for a multi-file git write firing the handler more than once.

With 10 namespaces on a ~25-session license, I reproducibly run out of sessions (around the 7th activation in the one-by-one case).

Steps to Reproduce

  1. Create a multi-root workspace (client-side editing) with N folders, each pointing to a different namespace on the same server via a folder-level objectscript.conn (server + ns + active), with the server defined in the .code-workspace file.
  2. Start with all connections inactive, then activate them one at a time by setting objectscript.conn.active = true in each folder's .vscode/settings.json.
  3. Watch the CSP session count in the server's management portal.

Alternatively, activate them all at once — e.g. git revert/git restore a commit that set every folder's objectscript.conn.active back to true — and observe the session count spike well past the number of namespaces in a single go.

Observed CSP session growth (10 namespaces, activated one at a time)

Activate namespace # CSP sessions delta
1 1 +1
2 3 +2
3 6 +3
4 11 +5
5 17 +6
6 23 +6
7 ⚡ HTTP parse error — session/license limit reached (see below)

The per-activation cost grows with the number of already-active connections — the signature of "reconnect all active folders on every config change."

License Usage (Management Portal → System Operation → License Usage):

Image

Error observed when the limit is hit

Once the session/license cap is reached, requests no longer return a clean HTTP status. Instead the Node HTTP client throws:

Error: Parse Error: Data after `Connection: close`

This is a transport / HTTP-parse-level failure (Node's llhttp strict parser rejecting a malformed or prematurely-closed response from the web gateway once it can no longer serve the request) — not an HTTP 401 or 503. Because it is a thrown error rather than a response object, it bypasses the extension's response.status === 503 ("Check License Usage") handling in src/api/index.ts and instead lands in the generic .catch in checkConnection, which calls handleError(...) and then setConnectionState(configName, false).

Expected

Activating N namespaces on one server+user should consume ~1 shared CSP session (they already share one cookie jar entry — see below), or at most N. It should not scale super-linearly, and reconnects should not strand server sessions.

Root cause analysis (code references, current master)

1. Any change to objectscript.conn re-checks every workspace folder with clearCookies=true.

src/extension.ts onDidChangeConfiguration:

if (affectsConfiguration("objectscript.conn") || affectsConfiguration("intersystems.servers")) {
  ...
  for await (const folder of vscode.workspace.workspaceFolders ?? []) {
    ...
    await checkConnection(true, folder.uri, true);   // clearCookies = true
  }

Activating one connection edits objectscript.conn, so this loop reconnects all active folders.

2. checkConnection(clearCookies=true) deletes the local session cookie, but never logs the server session out.

src/api/index.ts:

public clearCookies(): void {
  cookiesMap.delete(this.mapKey());   // local only — no IRISLogout/CacheLogout
}

With the cookie gone, serverInfo()request() fires a bootstrap HEAD /api/atelier/ with Basic auth, and IRIS mints a new CSPSESSIONID. The previously-created server session is not logged out; it lingers until the CSP session timeout. Explicit IRISLogout/CacheLogout only happens in deactivate() (extension shutdown), and only for the single current cookie per folder — never for these intermediate orphaned sessions.

3. The shared cookie key makes the loop thrash a single slot.

src/api/index.ts mapKey() keys the cookie jar by username@host:port/pathPrefixnamespace is intentionally excluded:

private mapKey(): string {
  const { host, port, username } = this.config;
  ...
  return `${username}@${host}:${port}${pathPrefix}`;
}

So all namespace folders on the same server+user share one cookie slot. Inside the config-change loop (for await, sequential) over K active folders, each iteration clearCookies() deletes the previous folder's cookie and re-handshakes — orphaning ~K−1 sessions in a single pass. The sharing that should save sessions instead causes churn.

Net effect:

  • One-by-one: activating connection #K triggers a loop over the ~K active folders, minting ~K fresh HEAD logins, all but one orphaned server-side. Orphans accumulate across activations until the license cap is reached — matching the observed 1, 3, 6, 11, 17, 23 progression.
  • All at once (git revert/restore): the same loop runs over all N folders in one pass (and a multi-file settings write may fire the handler more than once), stranding ~N−1 sessions per pass — enough to blow past the license on its own.

Side effect: conn.active gets rewritten to false in .vscode/settings.json

When a reconnect fails — including the Parse Error: Data after `Connection: close` thrown once the session limit is reached, as well as 401s that can't re-auth or timeouts — checkConnection persists the failure via setConnectionState(configName, false) (src/extension.ts). Because objectscript.conn is defined at folder level, the write targets ConfigurationTarget.WorkspaceFolder, rewriting each folder's own .vscode/settings.json — which shows up as unexpected git changes. So hitting the session limit silently disables connections on disk. (Note the extension's dedicated 503 "Check License Usage" branch never runs here, because the limit surfaces as a thrown parse error rather than a 503 response.)

Suggested directions

  • In the onDidChangeConfiguration loop, skip folders whose resolved connection details are unchanged (only re-check the folder(s) that actually changed), instead of clearCookies-reconnecting every folder.
  • Before dropping a cookie in clearCookies() / a forced reconnect, send IRISLogout: "end" (or CacheLogout: "end" for < 2018.2) for the session being discarded, so it isn't stranded server-side.
  • Avoid flipping conn.active to false on transient / session-limit failures — including the Parse Error: Data after `Connection: close` case, which currently isn't recognized as a license/session condition and is treated like any other fatal error — so users don't get spurious settings writes when they're merely over the session limit.

Related

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions