Skip to content

fix: reserve LightGBM worker ports until network init - #2595

Merged
Rana Singh (ranadeepsingh) merged 4 commits into
microsoft:masterfrom
ranadeepsingh:copilot/ancient-2233-network-port
Aug 7, 2026
Merged

fix: reserve LightGBM worker ports until network init#2595
Rana Singh (ranadeepsingh) merged 4 commits into
microsoft:masterfrom
ranadeepsingh:copilot/ancient-2233-network-port

Conversation

@ranadeepsingh

@ranadeepsingh Rana Singh (ranadeepsingh) commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • keep each JVM-selected LightGBM worker port reserved through driver topology exchange and partition data preparation
  • release the reservation immediately before LGBM_NetworkInit binds the native listener
  • treat only BindException as port contention; propagate configuration and cleanup failures instead of silently falling back
  • clean reservations on training, helper, initialization, preparation, and failure paths while preserving the existing three-field NetworkTopologyInfo API
  • add deterministic and concurrent regression coverage for competing listeners, helper/failure cleanup, close retries, and non-contention errors

Why this replaces the ancient PR

This is a current-master recreation of the still-valid race fix proposed in #2233 for #2230. The original PR is based on an ancient contributor branch and retained an unresolved broad Exception catch around socket cleanup. This implementation was rewritten against current NetworkManager/BasePartitionTask, narrows fallback to actual bind contention, and adds the missing lifecycle and concurrency coverage. It does not close or modify the original PR.

Fixes #2230

Validation

  • lightgbm/compile
  • lightgbm/testOnly com.microsoft.azure.synapse.ml.lightgbm.split1.NetworkManagerSuite (5 tests)
  • lightgbm/testOnly com.microsoft.azure.synapse.ml.lightgbm.split3.VerifyLightGBMClassifierStreamBasic -- -z "PimaIndian.csv" (2 tests; includes helper-task startup)
  • scalastyle and test:scalastyle across all modules
  • black==22.3.0 --check --extend-exclude 'docs/' .

Copilot AI lite review requested due to automatic review settings August 1, 2026 15:00
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

Hey Rana Singh (@ranadeepsingh) 👋!
Thank you so much for contributing to our repository 🙌.
Someone from SynapseML Team will be reviewing this pull request soon.

We use semantic commit messages to streamline the release process.
Before your pull request can be merged, you should make sure your first commit and PR title start with a semantic prefix.
This helps us to create release messages and credit you for your hard work!

Examples of commit messages with semantic prefixes:

  • fix: Fix LightGBM crashes with empty partitions
  • feat: Make HTTP on Spark back-offs configurable
  • docs: Update Spark Serving usage
  • build: Add codecov support
  • perf: improve LightGBM memory usage
  • refactor: make python code generation rely on classes
  • style: Remove nulls from CNTKModel
  • test: Add test coverage for CNTKModel

To test your commit locally, please follow our guild on building from source.
Check out the developer guide for additional guidance on testing your change.

@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a LightGBM training race where JVM-selected worker ports could be taken by another process between topology exchange / data preparation and the native LGBM_NetworkInit bind, by reserving each worker port until immediately before LightGBM binds it. It updates the LightGBM NetworkManager and task lifecycle cleanup to maintain reservations across initialization paths, and adds regression tests covering contention, cleanup, and concurrency.

Changes:

  • Add port-reservation lifecycle to NetworkTopologyInfo and wire it through NetworkManager so only BindException triggers port fallback and reservations are held until LGBM_NetworkInit.
  • Ensure reservations are released on helper, failure, and task cleanup paths in BasePartitionTask.
  • Add a new NetworkManagerSuite with deterministic and concurrent regression coverage for reservations, contention, and cleanup behavior.
Show a summary per file
File Description
lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/NetworkManager.scala Introduces port reservation/retention/release and narrows fallback to BindException; releases reservation immediately before LGBM_NetworkInit.
lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/BasePartitionTask.scala Extends reservation cleanup to more lifecycle paths (including initialization failures and outer task cleanup).
lightgbm/src/test/scala/com/microsoft/azure/synapse/ml/lightgbm/NetworkManagerSuite.scala Adds regression tests for reservation behavior, contention handling, close retryability, and concurrent reservation uniqueness.

Review details

Suppressed comments (1)

lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/BasePartitionTask.scala:208

  • If getTaskContext (or later initialization logic) throws, networkInfo.releasePortReservation() can throw in the finally and mask the original initialization failure. Preserve the primary exception by suppressing close failures when unwinding due to another throwable, while still propagating close failures when initialization otherwise succeeds.
    var initializationSucceeded = false
    try {
      // Return booster only from main worker to reduce network communication overhead
      val shouldReturnBooster = if (isEmptyPartition) false
        else if (!shouldExecuteTraining) false
  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Lite

@codecov-commenter

Codecov Comments Bot (codecov-commenter) commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.13889% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.32%. Comparing base (d3ef6e3) to head (5dab71c).

Files with missing lines Patch % Lines
...oft/azure/synapse/ml/lightgbm/NetworkManager.scala 94.91% 6 Missing ⚠️
.../azure/synapse/ml/lightgbm/BasePartitionTask.scala 96.15% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2595      +/-   ##
==========================================
+ Coverage   86.07%   86.32%   +0.25%     
==========================================
  Files         331      331              
  Lines       17654    17742      +88     
  Branches     1646     1660      +14     
==========================================
+ Hits        15195    15316     +121     
+ Misses       2459     2426      -33     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ranadeepsingh
Rana Singh (ranadeepsingh) force-pushed the copilot/ancient-2233-network-port branch from be4b995 to b936f20 Compare August 1, 2026 15:38
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Rana Singh (ranadeepsingh) added a commit to ranadeepsingh/SynapseML that referenced this pull request Aug 1, 2026
## Summary
Preserve primary task failures when port-reservation cleanup also fails, retry socket closure while the reservation remains reachable, and hold the exact advertised port during native network-init backoff.

## Prompting Intent
Resolve the two independent medium findings on PR microsoft#2595: prevent cleanup from masking primary failures or losing open sockets, and eliminate the native-init retry race without broad fallback catches. Add focused suppression, cleanup-retry, exact-port backoff, and contention regressions.

## Linked Sources
- Replacement pull request: microsoft#2595
- Original pull request: microsoft#2233
- Original issue: microsoft#2230
- Original unresolved review: microsoft#2233 (comment)

## Rationale
Retry only IOException failures declared by Socket.close, while a tightly scoped Throwable-preservation helper immediately rethrows the protected operation and records cleanup failures as suppressed. Re-reserve only the already-advertised port between native attempts; if contention wins, fail safely with the native error primary so Spark can renegotiate instead of retrying an unreserved or different port.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Rana Singh (ranadeepsingh) added a commit to ranadeepsingh/SynapseML that referenced this pull request Aug 1, 2026
## Summary
Carry the preceding native-init failure through backoff-reservation release, suppress close failures onto it, and continue the retry when the socket ultimately closes.

## Prompting Intent
Resolve the remaining medium review finding on PR microsoft#2595 without weakening cleanup reporting: a first close may fail while the immediate retry succeeds, so the native retry must proceed without replacing its original failure.

## Linked Sources
- Replacement pull request: microsoft#2595
- Original pull request: microsoft#2233
- Original issue: microsoft#2230

## Rationale
Use an explicit internal overload to preserve the existing test seam and pass the prior native failure only across the recursive handoff. Continue only after synchronized reservation state confirms the socket closed; otherwise throw the native failure with aggregated close errors suppressed so no unsafe bind occurs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

Re-review requested for commit 1d7694bfd9.

The remaining retry-handoff issue is fixed: the preceding native-init failure is carried through reservation release, close failures are suppressed onto it, and retry continues only when the socket ultimately closed. Added a regression where the first close throws and the second succeeds. Local targeted/integration/style checks passed, and Azure build 229247623 plus all 76 PR checks are green.

@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

Pushed 8e05851c2f — test-only change closing the remaining patch-coverage gap on the new error-handling paths.

Added to NetworkManagerSuite (4 tests):

  • LGBM_NetworkInit is never invoked when the advertised port cannot be released beforehand; the close failure propagates with the retry close failure suppressed.
  • A native-init retry whose backoff reservation cannot be closed aborts with the original native failure (close failure attached as suppressed) instead of letting the native layer bind a port the JVM still holds.
  • Port scanning stops after its 1k-port contention window.
  • Out-of-range base ports are rejected before any socket is allocated.

No production code changed. Locally: 15/15 NetworkManagerSuite tests pass, lightgbm/scalastyle and lightgbm/Test/scalastyle clean (0 errors).

Review threads: both outstanding Copilot comments about exception masking in finally blocks were already addressed by 4f5cdf9740 / 1d7694bfd9 (withCleanupPreservingPrimary / withCleanupOnFailurePreservingPrimary); I replied on each thread and marked them resolved.

On the retry design — I verified against LightGBM's src/network/socket_wrapper.hpp that TcpSocket::Bind does not set SO_REUSEADDR, so the JVM reservation probe has identical bind semantics to the native listener. That makes hard-failing when exact re-reservation loses the handoff race the correct behaviour rather than a fault-tolerance regression: the port is genuinely unavailable to the native layer too, and every peer has already been told that port number, so silently changing it would corrupt the negotiated topology. Failing the task lets Spark renegotiate.

This also matches a live customer-reported incident on Fabric (France Central, starter pool) where distributed LightGBM training fails with repeated java.net.ConnectException: Connection refused during network topology init — exactly the port-theft window this PR closes.

@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

Correction to my earlier comment.

I previously wrote that IcM 51000001093267 (Connection refused during LightGBM training) matches this PR. That was overstated, and I want to walk it back.

The incident's only error signature is java.net.ConnectException, which is a JVM-side exception. The only outbound JVM connections during LightGBM startup are executor-to-driver (NetworkManager.scala). The native executor-to-executor network that this PR fixes reports failures through LightGBMUtils.validate as a generic Exception, never as ConnectException. So that incident cannot be this bug.

The incident traces to a separate defect: the driver serves the topology exchange once and then closes its server socket, so every Spark task retry gets Connection refused, which overwrites the failure that actually caused the retry. That is addressed in #2612.

This PR still fixes a real and distinct bug — the executor-side listen port race — and the two are complementary. Apologies for the earlier conflation.

Rana Singh (ranadeepsingh) added a commit that referenced this pull request Aug 7, 2026
* ci: preserve sbt retry helper during release replay

## Summary
Stage the sbt retry helper outside the repository before switching to Spark release branches, and parameterize the shared cache template so it can invoke that stable path after rebase.

## Prompting Intent
Investigate why Spark 3.5 and Spark 4.1 compatibility checks still failed after PR #2608, reproduce the failure with PR #2595 changes, implement the complete hotfix, and validate the real release replay path.

## Linked Sources
- Failing PR: #2595
- Prior identity hotfix: #2608
- Failed Azure build: https://msdata.visualstudio.com/b9b2accc-2d1c-45b3-9d24-0eb5d78cc47f/_build/results?buildId=229969678

## Rationale
The rebased working tree intentionally comes from the Spark release branch, so master-only CI helpers cannot remain repository-relative. Copying the helper to Agent.TempDirectory preserves release-specific dependency resolution and avoids moving cache warming ahead of the rebase, where exact cache hits could hide missing release dependencies.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* ci: replay only release-relevant PR changes

## Summary
Replace commit-history rebasing with a three-way application of the synthetic PR merge tree's release-relevant patch onto each Spark release branch.

## Prompting Intent
Validate the compatibility hotfix with PR #2595's real source changes while ensuring CI-only commits do not conflict with old Spark branches that predate the current pipeline and helper files.

## Linked Sources
- Validation PR source: #2595
- Prior identity hotfix: #2608
- Failed Azure build: https://msdata.visualstudio.com/b9b2accc-2d1c-45b3-9d24-0eb5d78cc47f/_build/results?buildId=229969678

## Rationale
The compatibility job needs the effective PR content on the release tree, not unrelated CI and documentation commits. Building the patch from the synthetic merge commit preserves GitHub's merge result, handles source branches behind master, retains three-way conflict detection, and avoids requiring commit identity.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* ci: focus release compatibility on Spark 4.1 compilation

## Summary
Remove the redundant Spark 3.5 release matrix leg and replace broad Spark 4.1 runtime suites with full test compilation of the effective PR patch.

## Prompting Intent
Explain why the release compatibility jobs exist and keep fixing the failures exposed by validation PR #2610, accounting for master already targeting Spark 3.5.

## Linked Sources
- Original compatibility PR: #2550
- Streamlining PR: #2583
- Integration validation PR: #2610
- Azure validation build: https://msdata.visualstudio.com/b9b2accc-2d1c-45b3-9d24-0eb5d78cc47f/_build/results?buildId=229984834

## Rationale
Normal PR validation already compiles and tests master on Spark 3.5, so replaying onto the older spark3.5 maintenance snapshot duplicates coverage and introduces unrelated JVM drift. Spark 4.1 test compilation catches cross-version source and test API breakage, while the existing master test fan-out supplies runtime coverage without rerunning broad, memory-heavy suites on a constrained compatibility agent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: SynapseML CI <synapseml-ci@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

## Summary
Keep each JVM socket reservation alive through topology exchange and data preparation, then release it immediately before native LightGBM network initialization. Clean up training, helper, and failure paths and add focused contention and cleanup regressions.

## Prompting Intent
Recreate the still-valid fix from ancient PR microsoft#2233 on current master, resolve issue microsoft#2230 without broad exception handling or silent fallback, preserve public API compatibility, and validate port competition and cleanup using TDD.

## Linked Sources
- Original pull request: microsoft#2233
- Original issue: microsoft#2230
- Unresolved review comment: microsoft#2233 (comment)

## Rationale
Store the reservation as transient internal state on the existing three-field topology type so callers remain source compatible. Advance to another port only for BindException; propagate other failures. Release at the unavoidable JVM-to-native handoff because the Java socket cannot be transferred to LightGBM, with final cleanup covering initialization, preparation, helper, and failure paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Preserve primary task failures when port-reservation cleanup also fails, retry socket closure while the reservation remains reachable, and hold the exact advertised port during native network-init backoff.

## Prompting Intent
Resolve the two independent medium findings on PR microsoft#2595: prevent cleanup from masking primary failures or losing open sockets, and eliminate the native-init retry race without broad fallback catches. Add focused suppression, cleanup-retry, exact-port backoff, and contention regressions.

## Linked Sources
- Replacement pull request: microsoft#2595
- Original pull request: microsoft#2233
- Original issue: microsoft#2230
- Original unresolved review: microsoft#2233 (comment)

## Rationale
Retry only IOException failures declared by Socket.close, while a tightly scoped Throwable-preservation helper immediately rethrows the protected operation and records cleanup failures as suppressed. Re-reserve only the already-advertised port between native attempts; if contention wins, fail safely with the native error primary so Spark can renegotiate instead of retrying an unreserved or different port.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Carry the preceding native-init failure through backoff-reservation release, suppress close failures onto it, and continue the retry when the socket ultimately closes.

## Prompting Intent
Resolve the remaining medium review finding on PR microsoft#2595 without weakening cleanup reporting: a first close may fail while the immediate retry succeeds, so the native retry must proceed without replacing its original failure.

## Linked Sources
- Replacement pull request: microsoft#2595
- Original pull request: microsoft#2233
- Original issue: microsoft#2230

## Rationale
Use an explicit internal overload to preserve the existing test seam and pass the prior native failure only across the recursive handoff. Continue only after synchronized reservation state confirms the socket closed; otherwise throw the native failure with aggregated close errors suppressed so no unsafe bind occurs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rSuite

Adds four regression tests for the error paths introduced by the port
reservation handoff that had no coverage:

- Native init is never invoked when the advertised port cannot be released
  before LGBM_NetworkInit; the close failure propagates with the retry close
  failure suppressed.
- A native-init retry whose backoff reservation cannot be closed aborts with
  the original native failure (close failure suppressed) rather than binding
  a port that is still held.
- Port scanning stops after its 1k-port contention window and never scans
  past MaxPort.
- Out-of-range base ports are rejected before any socket is allocated.

No production code changes: this only closes the patch-coverage gap in
NetworkManager's release/retry paths. All 15 NetworkManagerSuite tests and
lightgbm scalastyle (main + test) pass locally.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1272df23-c4f3-4048-8789-0320ea286628
Copilot AI review requested due to automatic review settings August 7, 2026 07:03
@ranadeepsingh
Rana Singh (ranadeepsingh) force-pushed the copilot/ancient-2233-network-port branch from 438de70 to 5dab71c Compare August 7, 2026 07:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (1)

lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/NetworkManager.scala:467

  • The exception text here is misleading: this branch is reached because the scan exceeded MaxPort, not because basePort itself is out of range. Consider rewording the message to reflect port exhaustion so users can distinguish it from a misconfigured base port.
          if (nextPort > LightGBMConstants.MaxPort) {
            throw new Exception(s"Error: port $basePort out of range, " +
              "possibly due to networking or firewall issues")
          }
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@ranadeepsingh
Rana Singh (ranadeepsingh) merged commit 2d6b392 into microsoft:master Aug 7, 2026
75 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Binding port failed

4 participants