Skip to content

fix: scale LightGBM validation data transfer - #2664

Open
Rana Singh (ranadeepsingh) wants to merge 20 commits into
microsoft:masterfrom
ranadeepsingh:copilot/issue-2294-validation-data-scaling
Open

fix: scale LightGBM validation data transfer#2664
Rana Singh (ranadeepsingh) wants to merge 20 commits into
microsoft:masterfrom
ranadeepsingh:copilot/issue-2294-validation-data-scaling

Conversation

@ranadeepsingh

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

Copy link
Copy Markdown
Collaborator

What this fixes

Closes #2294.

Large sparse validation sets supplied through validationIndicatorCol were preprocessed with collect() and broadcast as Array[Row], making Spark return the complete validation set to the driver and causing driver result-size/heap failures.

Design

  • Materialize every preprocessed validation row to a driver-local spool through DataFrame.foreachPartition and bounded 64 KiB driver copy buffers.
  • Preserve partition ordering and exact rows; no sampling or validation sharding is introduced.
  • Broadcast only an authenticated, time-bounded stream descriptor.
  • Stream the complete spool to every executor/task that creates a native LightGBM validation Dataset in both streaming and bulk transfer modes.
  • Bound concurrent ingest/serve transfers and clean sockets, attempt files, spool directories, and iterator resources on success and failure.
  • Accept the first complete Spark attempt for each partition so retries/speculation cannot overwrite committed partition data.

LightGBM 3.3.5 calculates validation metrics locally, including non-decomposable metrics such as AUC. Each active native worker therefore still receives the complete validation set; changing that would alter validation and early-stopping semantics.

Resource lifecycle

  • close() is the single bounded terminal operation: it stops acceptance, closes active clients, terminates the fixed executor, surfaces serving failures, and deletes the spool only after confirmed termination.
  • Close clients before executor shutdown, so stalled socket writes are forcibly unblocked; a nonterminating executor fails explicitly and leaves the spool intact.
  • Keep spool ownership exception-safe across ingest bind/executor failure, serving bind/executor failure, and serving start failure.
  • Treat EOF, socket disconnects, authentication rejection, and accept polling timeouts as expected cancellation/idle behavior while preserving genuine I/O, spool-read, and cleanup failures.
  • Bound copy buffers to 64 KiB, concurrent transfers to eight, and ingest/serve thread pools to nine daemon threads each; no per-row or per-transfer result collection is retained.

Compatibility

  • No public estimator parameters, JVM method signatures, or serialized parameter shapes change.
  • TrainingContext.validationData retains its existing Option[Broadcast[Array[Row]]] type and constructor position.
  • Legacy broadcast arrays remain readable; the new broadcast contains only a small versioned descriptor.
  • Copy, estimator/model persistence, transform schema, sparse vectors, and both transfer modes are covered.
  • Null validation indicators now fail explicitly instead of being silently excluded by SQL null filtering.

Validation

  • SynapseML local setup doctor: JDK 11 available; Spark 3.5 / Scala 2.12 confirmed. WSL cannot parse the Windows-path worktree .git indirection, but Windows Git confirms branch/base state.
  • lightgbm/compile: passed.
  • lightgbm/Test/compile: passed.
  • lightgbm/scalastyle: passed with 0 findings.
  • lightgbm/Test/scalastyle: passed with 0 findings.
  • python3 -m black --check --extend-exclude docs/ .: passed, 194 files unchanged.
  • ValidationDataServerLifecycleSuite: 28/28 passed; LightGBMValidationDataSuite: 4/4 passed.
    • Deterministic ingest/serving socket, executor-construction, and server-start failure cleanup.
    • Stalled output client forced closed before executor termination/spool deletion.
    • Cancelled speculative client and idle accept timeout do not poison a later successful transfer.
    • Genuine serving I/O failure remains visible.
    • Nontermination fails explicitly and preserves the spool.
    • 256 rows, 4,096-dimensional sparse vectors with 1,024 deterministic nonzeros, 64 validation rows, four partitions.
    • spark.driver.maxResultSize=128k; a temporary legacy-collect replay failed deterministically at 156.4 KiB for one task and 626.4 KiB across four tasks.
    • Streaming estimator fit, schema, copy, estimator/model persistence, scoring, bulk mode, null validation indicators, and cleanup.
  • Confirmed the old validation collect()/large-row broadcast path is absent.
  • Existing VerifyLightGBMClassifierStream validation test remains unavailable because au3_25000.csv is not present in this worktree.
  • Code generation was not rerun because no wrapper or Python API surface changed.

Merge coordination

PR #2664 overlaps StreamingPartitionTask.scala with #2662. Merge #2662 first, then rebase #2664 onto the updated master. During that rebase, retain both #2662's native LightGBM Dataset failure cleanup and #2664's closeable validation-row iterator handling.

Residual risk

  • Exact semantics necessarily retain O(validation data) driver disk usage, full validation network transfer to each active native worker, and a complete native validation Dataset per worker.
  • This was validated locally, not at Fabric production scale or against a modified native LightGBM implementation.
  • Exact-head Azure validation is requested after the final review; merge readiness remains pending until it completes.

Exact-head review follow-up

Copilot reviewed head 3eaca963317d214c7af6c9f05aa8ee1c01832bc9 and reported two active findings plus one suppressed executor-read finding. Head db086b50f6a42007d6004437a4f27449942c6295 addresses all three:

  • Both socket ingest and executor-read paths reject negative row lengths other than the -1 end marker with a clear IOException before copying or allocating.
  • No arbitrary upper bound was added because there is no existing serialized-row frame-size invariant; imposing one could reject valid large sparse rows and change supported semantics.
  • Broadcast destruction and validation-server close use the existing primary-preserving cleanup pattern, so training or await() failures remain primary and cleanup failures are suppressed.
  • Deterministic tests cover malformed lengths in both network directions and simultaneous broadcast/server cleanup failures for both training and await() failures.
  • Scala 2.12 main/test compile passed on master; the complete PR patch also applied cleanly and main/test compiled on the live spark4.0 Scala 2.13 branch with JDK 17.

New-head socket-setup follow-up

Copilot reviewed db086b50f6a42007d6004437a4f27449942c6295 and surfaced one suppressed socket-setup leak. Head 965f55d6d9f4680b1eddc4fd7872784d171fff2f wraps executor stream authentication/input initialization in failure-preserving socket cleanup, so getOutputStream, token write/flush, configuration, or getInputStream failures cannot leak the connected socket. A deterministic failing-output socket test verifies the original exception remains primary and the socket is closed. Final evidence is 18/18 lifecycle/integration tests, Scala 2.12 main/test compile, full-patch Spark 4.0/Scala 2.13 main/test compile, both scalastyle tasks, and Black.

Timeout and row-count follow-up

Copilot reviewed 965f55d6d9f4680b1eddc4fd7872784d171fff2f and surfaced two suppressed clarity findings. Head bcae907868ee379c811c670b18a0c36b9bd5ed5c uses an explicit seconds-to-milliseconds conversion independent of accept polling, and validates the downstream native Int row-count range with a targeted IllegalArgumentException instead of exposing ArithmeticException. Deterministic tests cover both. Final Scala 2.12 validation passes 20/20 lifecycle/integration tests, main/test compile, both scalastyle tasks, and Black.

Fractional deadline and iterator cleanup follow-up

Head 3a36dee3bab542510b0ba990a5a18dd814bc57c9 uses millisecond-precise ingest deadlines and primary-preserving iterator cleanup in streaming and bulk consumers. The active thread was replied to and resolved; both suppressed findings are covered by a deterministic processing-plus-close failure test. Scala 2.12 main/test compile, both scalastyle tasks, and 22/22 tests pass. The full patch compiled on Spark 4.0/Scala 2.13 at the preceding head; these final changes use APIs common to both branches.

Maintainability audit on exact head

Head 23c503f71b07681ad318c46f5ed1b6f255d158bf completes the adversarial simplicity audit:

  • Removed the separate server await() phase, serving transfer Future queue, ingest Future retention, per-partition row-count map, completed-partition set, and brittle executor/socket test hooks.
  • Replaced ingest result retention with a fixed-memory semaphore barrier, row totals with AtomicLong, and completion with a range-validated AtomicInteger.
  • Preserved primary failures while retaining close failures as suppressed exceptions, including simultaneous stream/input/socket close failures.
  • Rejected unsupported descriptor versions and invalid partition IDs explicitly while preserving legacy row broadcasts and all public JVM/serialized shapes.
  • Kept unavoidable costs explicit: O(validation data) driver disk, one complete network transfer per native Dataset owner, and native complete validation data per worker. No arbitrary row cap, sampling, or sharding was introduced.

Validation on this exact diff:

  • Spark 3.5 / Scala 2.12 / JDK 11: main/test compile, 27/27 lifecycle tests, 3/3 public estimator tests, and both scalastyle tasks passed.
  • Spark 4.1 / Scala 2.13 / JDK 17: the full patch applied to live upstream/spark4.1; main/test compile, 27/27 lifecycle tests, 3/3 public estimator tests, and both scalastyle tasks passed.
  • Black and git diff --check passed; codegen remains unaffected because no wrapper or Python API changed.
  • A separate combined replay of PR fix: clean up failed LightGBM streaming datasets #2662 plus the intended conflict resolution passed main/test compile, StreamingDatasetLifecycleSuite (1/1), and LightGBMValidationDataSuite (3/3).

Merge order remains #2662 first, then rebase #2664. The rebase must preserve #2662's owned native Dataset cleanup around #2664's streamed validation row source in StreamingPartitionTask.initializeValidationDataset.

Exact-head backlog follow-up

Copilot reviewed 23c503f71b07681ad318c46f5ed1b6f255d158bf and identified the fixed 50-connection ingest accept backlog as a concurrency bottleneck for validation DataFrames with more than 50 partitions. Head 303fb964888c7e4b5f4ba6282eb7babfc9fa2832 passes the exact validation partition count to the listener; the socket implementation still clamps small values to the existing default and the operating system retains its own maximum.

A deterministic regression captured 50 before the fix for 73 partitions and 73 after it. Spark 3.5/Scala 2.12 and Spark 4.1/Scala 2.13 main/test compile, 27/27 lifecycle tests, 3/3 public estimator tests, both scalastyle tasks, and Black passed. The review thread was replied to and resolved.

Suppressed wide-row null-check follow-up

The exact-head review of 303fb964888c7e4b5f4ba6282eb7babfc9fa2832 suppressed one previously missed finding: null-indicator detection returned a complete filtered row to the driver. Head b469be2b811ceb98fdbdcf995a2bec51602c5db8 selects only the boolean indicator before head(1), preserving explicit null rejection without materializing labels or feature vectors.

The public estimator regression now uses deterministic one-megabyte dense feature vectors under spark.driver.maxResultSize=256k; it fails before the projection and returns the intended IllegalArgumentException after it. Spark 3.5/Scala 2.12 and Spark 4.1/Scala 2.13 main/test compile, 27/27 lifecycle tests, 3/3 public estimator tests, both scalastyle tasks, and Black passed.

Suppressed serializer-order follow-up

The exact-head review of b469be2b811ceb98fdbdcf995a2bec51602c5db8 suppressed one previously missed iterator-construction leak: the executor opened its validation socket/input before acquiring a Spark serializer instance. Head 00330a487b2955f31afdb05bdef7967c58b72444 acquires the serializer first, before any network resource exists.

A deterministic ephemeral-listener regression injects a serializer-construction failure and proves the original exception is preserved with no connection attempt. Spark 3.5/Scala 2.12 and Spark 4.1/Scala 2.13 main/test compile, 27/27 lifecycle tests, 3/3 public estimator tests, both scalastyle tasks, and Black passed.

Suppressed subsecond ingest-timeout follow-up

The exact-head review of 00330a487b2955f31afdb05bdef7967c58b72444 suppressed one previously missed timeout mismatch: ingest replaced every configured accept timeout with a fixed one-second poll. Head 864f09efc1d56983b4286750dcc73147a90661e1 uses the smaller of the configured timeout and one second, so polling remains efficient without delaying subsecond deadlines.

A deterministic construction-path regression observes the listener timeout at 250 milliseconds and preserves socket/spool cleanup. Spark 3.5/Scala 2.12 and Spark 4.1/Scala 2.13 main/test compile, 27/27 lifecycle tests, 3/3 public estimator tests, both scalastyle tasks, and Black passed.

Azure coverage-timeout follow-up

Azure build 231779254 validated head 864f09efc1d56983b4286750dcc73147a90661e1 under scoverage. ValidationDataServerLifecycleSuite completed in about 14 seconds, but the original 16,000-row estimator regression consumed each 20-minute test timeout and caused both 30-minute Unit Test attempts to exit 124 before the 80-minute lightgbm1 job cap.

Head 1db7714508886e6cebe578b176bf28913d5e3462 changes only LightGBMValidationDataSuite: 256 rows, four tasks, 4,096 features, 1,024 deterministic nonzeros, eight reference-sample rows, and a 128 KiB driver result limit. A temporary replay of the legacy collect path failed at 156.4 KiB for one task (626.4 KiB total), while the streamed public estimator path passed.

Validation:

  • Exact coverage-mode lightgbm1 package command exercised the resized suite in 31.169 seconds and the lifecycle suite in 78.908 seconds; the overall local package command exposed 19 unrelated pre-existing split1 failures.
  • The coverage-mode command restricted to those two affected suites passed.
  • JDK 11 main/test compile and both scalastyle tasks passed.
  • Spark 4.1 / Scala 2.13 main/test compile, both scalastyle tasks, 27/27 lifecycle tests, and 3/3 estimator tests passed.
  • No production logic changed.

Exact-head cancellation, spool fallback, and bulk-helper follow-up

Heads 61013c25d88b28b6ca67d89600ec331e92a89e36, 263da875915bd7e18dbb9c70e12ffd78044e1635, and 85ec1fc111e6676ea7f29fd57cc5632f5ef41610 close the remaining coverage and exact-head review findings:

  • The stalled-client test double treats executor interruption as expected only after its tracked socket is closed, eliminating the uncaught coverage-thread error while retaining unexpected early interruptions.
  • Default spool creation still prefers user.dir, but atomically falls back to the JVM temporary directory when that parent is unusable; dual failures preserve the preferred error as suppressed.
  • Bulk single-dataset helpers no longer download the complete validation spool. Only the active task per executor reads it; helpers decrement the existing validation synchronization latch. Bulk per-task Dataset mode remains unchanged and transfers the complete validation set to every native Dataset owner.
  • The short accept-poll timeout test now uses a separate five-second client read timeout, preventing coverage overhead from creating a false socket timeout.

Exact-head validation:

  • Spark 3.5 / Scala 2.12 / JDK 11: main/test compile and both scalastyle tasks passed; 28/28 lifecycle tests and 4/4 public estimator tests passed.
  • Coverage mode: the combined lifecycle/integration selection passed 32/32 with no uncaught interruption.
  • Spark 4.1 / Scala 2.13 / JDK 17: full-patch main/test compile, both scalastyle tasks, and 4/4 public estimator tests passed; the exact fallback and stalled-client tests also passed.
  • Copilot reviewed exact head 85ec1fc111e6676ea7f29fd57cc5632f5ef41610 with zero generated or suppressed findings; all review threads are resolved.

Merge order remains #2662 first, then rebase #2664, preserving #2662's native Dataset ownership cleanup in the overlapping StreamingPartitionTask.scala path.

Rebase onto current master

Head 395bd9aa2c899a836bc5cbcb762b9fec47b1ef79 is rebased onto origin/master 9f152c2ef78d9975ff4f855e1d05b85cb358563e.

  • The rebase completed without conflicts.
  • git range-diff matched all 20 commits one-for-one, and the six intended LightGBM source/test files are byte-identical to pre-rebase head 85ec1fc111e6676ea7f29fd57cc5632f5ef41610.
  • JDK 11 validation passed: lightgbm/Test/compile, 4/4 LightGBMValidationDataSuite tests, and the filtered stalled-serving-client lifecycle test.
  • git diff --check passed and the worktree was clean.
  • origin/master was fetched again immediately before the lease-protected force push and remained at the recorded target SHA.

PR #2662 remains open and was neither merged nor incorporated.

@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

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
Rana Singh (ranadeepsingh) force-pushed the copilot/issue-2294-validation-data-scaling branch from fbe4a0a to b33dcc4 Compare August 18, 2026 12:49
Rana Singh (ranadeepsingh) pushed a commit to ranadeepsingh/SynapseML that referenced this pull request Aug 18, 2026
## Summary
Track and forcibly close active validation clients, use daemon executors with verified termination, and make spool ownership exception-safe across socket, executor, and server-start failures. Add deterministic lifecycle coverage for stalled and cancelled clients, construction failures, accept timeouts, real serving failures, and nontermination.

## Prompting Intent
Fix the resource-lifecycle blockers found in PR microsoft#2664 without changing exact LightGBM validation semantics: prevent blocked socket writes and JVM thread leaks, delete spools on every safe construction-failure path, preserve genuine serving failures, and tolerate expected task cancellation or speculation.

## Linked Sources
- Issue: microsoft#2294
- Draft PR and lifecycle review: microsoft#2664
- Related issue: microsoft#924
- Related issue: microsoft#978

## Rationale
Closing tracked sockets before executor shutdown is the only reliable way to unblock socket writes; thread interruption alone is insufficient. Spool deletion is gated on confirmed executor termination so no worker can read deleted files. Expected socket disconnects and accept polling timeouts are nonterminal, while file and generic I/O failures remain visible to await(). Injectable resource factories make every ownership transition deterministic to test without environmental port races.

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

Rana Singh (ranadeepsingh) commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

Lifecycle follow-up pushed at 3eaca96. Active ingest/serve sockets are now closed before executor shutdown; spool deletion requires confirmed termination; construction ownership is covered through bind, executor, and start failures; cancellation/accept timeouts remain nonterminal while real I/O failures propagate. JDK 11 compile/test-compile and both scalastyle tasks passed, Black passed, and the two targeted suites passed 13/13. Copilot automated review was requested and polled twice for 10 minutes but no review exists for this head. Azure was intentionally not triggered. Merge #2662 first, then rebase #2664 and preserve both cleanup changes.

@ranadeepsingh
Rana Singh (ranadeepsingh) marked this pull request as ready for review August 18, 2026 18:59
Copilot AI lite review requested due to automatic review settings August 18, 2026 18:59
@azure-pipelines

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

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 LightGBM training failures when validationIndicatorCol is used with large/sparse validation sets by eliminating the driver-side collect() + broadcast of all validation rows. Instead, it spools preprocessed validation rows to driver-local disk and streams them to executors/tasks that construct native LightGBM validation Datasets, preserving full validation semantics (each worker still receives the complete validation set).

Changes:

  • Add a driver-side ValidationDataServer that ingests per-partition validation rows via sockets into a spool directory, then serves the spool to executors via authenticated streaming.
  • Update streaming and bulk partition tasks to consume validation rows via ValidationDataServer.read(...) (iterator + explicit close) and to use the streamed row count.
  • Add lifecycle and end-to-end tests covering cleanup, persistence, bulk/stream modes, and null validation indicators.
Show a summary per file
File Description
lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/ValidationDataServer.scala New spool + stream server/descriptor format for scalable validation data transfer.
lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/LightGBMBase.scala Replaces validation collect() broadcast path with server-backed broadcast descriptor and explicit null-indicator rejection.
lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/StreamingPartitionTask.scala Streams validation rows into the shared streaming validation Dataset and closes the iterator.
lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/BulkPartitionTask.scala Streams validation rows for bulk-mode aggregation and closes the iterator.
lightgbm/src/test/scala/com/microsoft/azure/synapse/ml/lightgbm/split1/ValidationDataServerLifecycleSuite.scala New deterministic tests for server lifecycle cleanup and failure handling.
lightgbm/src/test/scala/com/microsoft/azure/synapse/ml/lightgbm/split1/LightGBMValidationDataSuite.scala New integration tests ensuring validation streaming avoids driver result-size issues and covers persistence/bulk/null-indicator behavior.

Review details

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Suppressed comments (1)

lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/ValidationDataServer.scala:689

  • Row lengths come from the network stream; a negative value other than the EndOfStream marker should be rejected explicitly. Without a guard this can throw NegativeArraySizeException (or lead to unexpected allocation behavior) instead of a clear I/O error.
          val length = input.readInt()
          if (length == EndOfStream) {
            close()
            None
          } else {
  • Files reviewed: 6/6 changed files
  • Comments generated: 2
  • Review effort level: Lite

Rana Singh (ranadeepsingh) pushed a commit to ranadeepsingh/SynapseML that referenced this pull request Aug 18, 2026
## Summary
Reject malformed negative row lengths in both validation ingest and executor read paths, and preserve training or await failures when broadcast and server cleanup also fail. Add deterministic malformed-frame and exception-suppression coverage.

## Prompting Intent
Resolve every exact-head Copilot review finding on PR microsoft#2664 without adding a semantics-changing frame-size cap: validate network-provided row lengths before allocation or copy, keep primary training diagnostics intact across cleanup, test both active and suppressed findings, and maintain cross-version compatibility.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Frame-length review: microsoft#2664 (comment)
- Cleanup review: microsoft#2664 (comment)
- Suppressed executor-read finding: Copilot review 4964793710 on PR microsoft#2664

## Rationale
A shared row-length decoder applies the protocol invariant consistently without imposing an arbitrary upper bound that could reject valid serialized sparse rows. Existing cleanup helpers attach broadcast and server cleanup failures to training or await exceptions, retaining actionable root-cause diagnostics while still exposing cleanup evidence. The helper is package-private and leaves public JVM and serialized APIs unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 18, 2026 20:13
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

Suppressed finding from Copilot review 4964793710 is explicitly fixed in db086b5: executor-side validation row lengths now pass through the same protocol validator as ingest, rejecting negative values other than the -1 end marker before allocation. A deterministic malformed executor-stream test covers that exact path. No upper cap was invented because the protocol has no established maximum serialized-row size and a new cap could reject valid sparse rows. Both active threads were also fixed, replied to, and resolved. Local evidence: Scala 2.12 main/test compile; full patch on spark4.0 Scala 2.13 main/test compile; 17/17 lifecycle/integration tests; both scalastyle tasks; Black. Azure was not triggered. #2662 must still merge first, followed by rebasing #2664 while preserving both StreamingPartitionTask cleanup changes.

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)

Previously missed (1) — in code that hasn't changed since the last review.

lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/ValidationDataServer.scala:678

  • The ValidationDataServer.read(params) iterator can leak an open socket if authentication/stream setup fails before input is initialized (e.g., socket.getOutputStream, writeUTF, or flush throws). Wrap initialization in a try/catch that closes the socket on failure and only keep input as a field.
      private val socket = connect(params.host, params.port, params.timeoutMillis)
      socket.setKeepAlive(true)
      socket.setTcpNoDelay(true)
      socket.setSoTimeout(params.timeoutMillis)
      private val auth = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream))
      auth.writeUTF(params.token)
      auth.flush()
      private val input = new DataInputStream(new BufferedInputStream(socket.getInputStream))
  • Files reviewed: 6/6 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).

Rana Singh (ranadeepsingh) pushed a commit to ranadeepsingh/SynapseML that referenced this pull request Aug 18, 2026
## Summary
Make executor validation-stream initialization exception-safe so authentication or input-stream setup failures close the connected socket. Add deterministic coverage for an authentication output failure.

## Prompting Intent
Resolve the suppressed exact-head Copilot finding on PR microsoft#2664, preserve the original setup exception, prove the socket is closed, revalidate Scala 2.12 and Scala 2.13 builds, and keep Azure validation delegated to the coordinating parent.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Suppressed review: Copilot review on commit db086b5

## Rationale
Stream initialization now uses the existing failure-preserving cleanup helper around socket configuration and authentication. This closes the socket on every pre-input failure while suppressing any close error onto the original setup exception, matching the lifecycle guarantees used elsewhere in the server.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 18, 2026 21:09
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

Addressed the new-head suppressed socket-setup finding in 965f55d. ValidationDataServer.read now closes its connected socket through the existing primary-preserving cleanup helper if socket configuration, authentication output/write/flush, or input-stream construction fails. A deterministic authentication-output failure test proves the original IOException remains primary and the socket is closed. Final validation: 18/18 lifecycle/integration tests; Scala 2.12 main/test compile; the full PR patch applied cleanly and main/test compiled on spark4.0 Scala 2.13/JDK 17; both scalastyle tasks; Black. Azure remains intentionally untriggered. #2662 still merges first, then #2664 rebases while retaining both StreamingPartitionTask cleanup changes.

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 (2)

Previously missed (2) — in code that hasn't changed since the last review.

lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/ValidationDataServer.scala:478

  • timeoutMillis is calculated using IngestPollTimeoutMillis as the seconds→milliseconds conversion factor. This couples the socket timeout semantics to the accept-poll constant, so changing the poll interval would silently change all timeouts. Use an explicit seconds→ms conversion instead (e.g., timeoutSeconds * 1000.0).
      val timeoutMillis = (timeoutSeconds * IngestPollTimeoutMillis).toLong
      socket.setSoTimeout(Math.max(IngestPollTimeoutMillis, timeoutMillis).min(Int.MaxValue).toInt)

lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/ValidationDataServer.scala:658

  • rowCount converts a Long to Int via Math.toIntExact, which will throw a bare ArithmeticException if validation row count exceeds Int.MaxValue. Since downstream LightGBM APIs require Int row counts anyway, it would be clearer to fail with a targeted IllegalArgumentException explaining the limit.
  def rowCount(data: Broadcast[Array[Row]]): Int = {
    ValidationDataParams.fromBroadcast(data)
      .map(params => Math.toIntExact(params.rowCount))
      .getOrElse(data.value.length)
  }
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Rana Singh (ranadeepsingh) pushed a commit to ranadeepsingh/SynapseML that referenced this pull request Aug 18, 2026
## Summary
Decouple seconds-to-milliseconds conversion from the accept-poll interval and replace overflow-prone validation row-count conversion with a targeted supported-range error. Add deterministic timeout and overflow tests.

## Prompting Intent
Resolve both suppressed findings from the exact-head Copilot review on PR microsoft#2664, keep timeout semantics stable if polling changes, and make the native Int row-count limit actionable without changing valid validation behavior.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Suppressed review: Copilot review on commit 965f55d

## Rationale
An explicit milliseconds-per-second constant documents the unit conversion independently of server polling. Validation counts already must fit the downstream native Int API, so validating the range and throwing a descriptive IllegalArgumentException preserves the existing limit while replacing an opaque ArithmeticException.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 18, 2026 21:44
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

Addressed both suppressed findings from the 965f55d review in bcae907. Socket timeout conversion now uses an explicit seconds-to-milliseconds constant rather than the accept-poll interval, and validation row counts outside 0..Int.MaxValue fail with a targeted IllegalArgumentException describing the downstream native limit. Deterministic tests cover 2.5 seconds -> 2500 ms and Int overflow. Final Scala 2.12 evidence: main/test compile, both scalastyle tasks, Black, 20/20 lifecycle/integration tests. Azure remains intentionally untriggered; #2662 still merges first, then #2664 rebases preserving both StreamingPartitionTask cleanup changes.

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 (2)

Previously missed (2) — in code that hasn't changed since the last review.

lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/StreamingPartitionTask.scala:165

  • rows.close() can throw (socket/input close), which would mask an exception from insertRowsIntoDataset and make failures harder to diagnose. Suppress non-fatal close failures so the primary training/validation error remains primary.
    } finally {
      rows.close()
    }

lightgbm/src/main/scala/com/microsoft/azure/synapse/ml/lightgbm/BulkPartitionTask.scala:53

  • rows.close() can throw (socket/input close), which would mask an exception from getChunkedColumns / mergeChunksIntoAggregatedArrays. Suppress non-fatal close failures so the primary error remains primary.
      } finally {
        rows.close()
      }
  • Files reviewed: 6/6 changed files
  • Comments generated: 1
  • Review effort level: Lite

Rana Singh (ranadeepsingh) pushed a commit to ranadeepsingh/SynapseML that referenced this pull request Aug 18, 2026
## Summary
Use millisecond-precise ingest deadlines and preserve row-processing failures when validation iterator cleanup also fails in streaming and bulk modes. Add deterministic regression coverage.

## Prompting Intent
Resolve the active and suppressed findings from the exact-head Copilot review on PR microsoft#2664 while keeping validation semantics and cleanup guarantees intact.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Active review: microsoft#2664 (comment)
- Suppressed findings: Copilot review on commit bcae907

## Rationale
The ingest deadline now derives from the already-normalized socket timeout, retaining fractional seconds. Both validation consumers share the existing primary-preserving cleanup helper, so close errors are suppressed onto processing failures rather than replacing them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 18, 2026 22:14
@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

Exact-head Azure validation is queued as build 231826848. ADO trigger metadata confirms reason=pullRequest, refs/pull/2664/merge, and pr.sourceSha=85ec1fc111e6676ea7f29fd57cc5632f5ef41610; status is currently inProgress. Final automated review covers this head with 0 generated and 0 suppressed findings, and there are 0 unresolved review threads. Remaining integration order: merge #2662 first, then rebase #2664 and preserve both overlapping StreamingPartitionTask.scala cleanup paths.

SynapseML CI and others added 20 commits August 19, 2026 03:40
## Summary
Replace the validationIndicatorCol collect-and-broadcast path with an exact, bounded driver-side spool and authenticated socket streaming to every native LightGBM validation Dataset. Add sparse public-estimator regressions for streaming and bulk modes, persistence/copy/schema coverage, cleanup checks, and explicit null-indicator rejection.

## Prompting Intent
Own GitHub issue microsoft#2294 end-to-end without sampling or changing validation semantics. Eliminate the driver-memory failure for large sparse validation data, retain the complete validation set on each active native worker, preserve public JVM and serialized compatibility, use DataFrame/Dataset APIs instead of an RDD workaround, and prove the former collect path is absent under a 256 KiB Spark driver result limit.

## Linked Sources
- Issue: microsoft#2294
- Related issue: microsoft#924
- Related issue: microsoft#978
- Related issue: microsoft#689
- Early stopping PR: microsoft#344
- Validation preprocessing PR: microsoft#706
- Worker/driver refactor PR: microsoft#1557
- Serialized reference Dataset PR: microsoft#1977
- Streaming mode default PR: microsoft#2088
- Local SynapseML code-review checklist and automated diff review

## Rationale
LightGBM 3.3.5 evaluates validation metrics locally and does not globally reduce validation scores, so sharding or sampling validation rows would change early-stopping and metric semantics. The implementation therefore spools every preprocessed validation row in partition order using fixed-size driver copy buffers and streams the complete spool to each worker that creates a validation Dataset. It retains the existing TrainingContext broadcast field as a small descriptor with a legacy-array fallback, bounding driver heap while accepting driver disk/network transfer and complete native worker validation memory as explicit trade-offs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Track and forcibly close active validation clients, use daemon executors with verified termination, and make spool ownership exception-safe across socket, executor, and server-start failures. Add deterministic lifecycle coverage for stalled and cancelled clients, construction failures, accept timeouts, real serving failures, and nontermination.

## Prompting Intent
Fix the resource-lifecycle blockers found in PR microsoft#2664 without changing exact LightGBM validation semantics: prevent blocked socket writes and JVM thread leaks, delete spools on every safe construction-failure path, preserve genuine serving failures, and tolerate expected task cancellation or speculation.

## Linked Sources
- Issue: microsoft#2294
- Draft PR and lifecycle review: microsoft#2664
- Related issue: microsoft#924
- Related issue: microsoft#978

## Rationale
Closing tracked sockets before executor shutdown is the only reliable way to unblock socket writes; thread interruption alone is insufficient. Spool deletion is gated on confirmed executor termination so no worker can read deleted files. Expected socket disconnects and accept polling timeouts are nonterminal, while file and generic I/O failures remain visible to await(). Injectable resource factories make every ownership transition deterministic to test without environmental port races.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Reject malformed negative row lengths in both validation ingest and executor read paths, and preserve training or await failures when broadcast and server cleanup also fail. Add deterministic malformed-frame and exception-suppression coverage.

## Prompting Intent
Resolve every exact-head Copilot review finding on PR microsoft#2664 without adding a semantics-changing frame-size cap: validate network-provided row lengths before allocation or copy, keep primary training diagnostics intact across cleanup, test both active and suppressed findings, and maintain cross-version compatibility.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Frame-length review: microsoft#2664 (comment)
- Cleanup review: microsoft#2664 (comment)
- Suppressed executor-read finding: Copilot review 4964793710 on PR microsoft#2664

## Rationale
A shared row-length decoder applies the protocol invariant consistently without imposing an arbitrary upper bound that could reject valid serialized sparse rows. Existing cleanup helpers attach broadcast and server cleanup failures to training or await exceptions, retaining actionable root-cause diagnostics while still exposing cleanup evidence. The helper is package-private and leaves public JVM and serialized APIs unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Make executor validation-stream initialization exception-safe so authentication or input-stream setup failures close the connected socket. Add deterministic coverage for an authentication output failure.

## Prompting Intent
Resolve the suppressed exact-head Copilot finding on PR microsoft#2664, preserve the original setup exception, prove the socket is closed, revalidate Scala 2.12 and Scala 2.13 builds, and keep Azure validation delegated to the coordinating parent.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Suppressed review: Copilot review on commit db086b5

## Rationale
Stream initialization now uses the existing failure-preserving cleanup helper around socket configuration and authentication. This closes the socket on every pre-input failure while suppressing any close error onto the original setup exception, matching the lifecycle guarantees used elsewhere in the server.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Decouple seconds-to-milliseconds conversion from the accept-poll interval and replace overflow-prone validation row-count conversion with a targeted supported-range error. Add deterministic timeout and overflow tests.

## Prompting Intent
Resolve both suppressed findings from the exact-head Copilot review on PR microsoft#2664, keep timeout semantics stable if polling changes, and make the native Int row-count limit actionable without changing valid validation behavior.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Suppressed review: Copilot review on commit 965f55d

## Rationale
An explicit milliseconds-per-second constant documents the unit conversion independently of server polling. Validation counts already must fit the downstream native Int API, so validating the range and throwing a descriptive IllegalArgumentException preserves the existing limit while replacing an opaque ArithmeticException.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Use millisecond-precise ingest deadlines and preserve row-processing failures when validation iterator cleanup also fails in streaming and bulk modes. Add deterministic regression coverage.

## Prompting Intent
Resolve the active and suppressed findings from the exact-head Copilot review on PR microsoft#2664 while keeping validation semantics and cleanup guarantees intact.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Active review: microsoft#2664 (comment)
- Suppressed findings: Copilot review on commit bcae907

## Rationale
The ingest deadline now derives from the already-normalized socket timeout, retaining fractional seconds. Both validation consumers share the existing primary-preserving cleanup helper, so close errors are suppressed onto processing failures rather than replacing them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Count the validation DataFrame partitions that actually execute ingest rather than the requested LightGBM task count, preventing false ingest timeouts when numTasks exceeds input partitions. Add regression coverage.

## Prompting Intent
Resolve the suppressed exact-head Copilot finding on PR microsoft#2664 without changing serving fan-out or validation semantics.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Suppressed finding: Copilot review on commit 3a36dee

## Rationale
DataFrame.foreachPartition creates one ingest client per actual DataFrame partition, while numTasks controls later training workers and serving backlog. Keeping those counts separate avoids waiting for clients Spark will never launch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Ensure validation collection timing always records its stop marker when validation-server creation fails, with deterministic failure-path coverage.

## Prompting Intent
Resolve the latest suppressed exact-head Copilot finding on PR microsoft#2664 while preserving the original server-creation failure.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Suppressed finding: Copilot review on commit a7af2a5

## Rationale
A small timing wrapper uses try/finally around server creation, so instrumentation remains accurate for failed runs without altering exception propagation or resource ownership.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Use the shared LightGBM test port allocator for validation-data integration tests to prevent deterministic port collisions across concurrent suites.

## Prompting Intent
Resolve all three suppressed exact-head Copilot findings on PR microsoft#2664, rerun targeted validation, and preserve the existing public estimator-path coverage.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Suppressed findings: Copilot review on commit 9b9870a

## Rationale
Reusing LightGBMTestUtils.getAndIncrementPort follows the repository's existing deterministic allocation pattern without changing estimator behavior or weakening integration coverage.

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

Preserve primary validation read and connect failures when socket cleanup also fails, make transfer-slot ownership explicit across interruption, and short-circuit null validation-indicator detection.

## Prompting Intent

Exhaust all active and suppressed exact-head review findings on PR microsoft#2664 without overengineering the validation streaming path or changing its public semantics.

## Linked Sources

- GitHub issue: microsoft#2294

- Pull request: microsoft#2664

- Active review: microsoft#2664 (comment)

## Rationale

Reuse the existing primary-preserving cleanup helper instead of adding another exception abstraction. Track semaphore ownership only after acquisition so interrupted waits cannot inflate permits. Use head(1) rather than count over a limited DataFrame to avoid an unnecessary aggregate while retaining the same null-rejection contract.

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

Preserve configured validation socket timeouts below one second, using a one-millisecond floor and explicit regression coverage.

## Prompting Intent

Resolve the final exact-head suppressed finding on PR microsoft#2664 and ensure documented fractional timeout values behave as configured.

## Linked Sources

- GitHub issue: microsoft#2294

- Pull request: microsoft#2664

- Suppressed review on commit 2e1b523

## Rationale

Ceiling the seconds-to-milliseconds conversion avoids turning a positive fractional timeout into Java's zero/infinite timeout, while a 1 ms floor preserves valid socket semantics without silently raising user values to the internal one-second accept-poll interval.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Collapse validation serving completion and cleanup into one bounded close operation, remove per-transfer Future retention and per-partition row-count maps, preserve stream cleanup failures, and validate protocol versions and partition IDs.

## Prompting Intent
Perform an adversarial simplicity and maintainability audit of PR microsoft#2664 after its review fixes. Remove avoidable lifecycle machinery, prove disk/network/thread bounds remain explicit, preserve exact validation semantics and compatibility, and verify the required merge composition with PR microsoft#2662.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Overlapping lifecycle pull request: microsoft#2662
- Exact-head automated review on b5b1ce6

## Rationale
A single close boundary can stop acceptance, close active sockets, terminate the fixed executor, surface serving failures, and delete the spool without a separate await phase or unbounded Future queues. Semaphore ownership provides a constant-memory ingest completion barrier. Driver disk and complete per-worker network transfer remain intentionally linear because native LightGBM requires every native validation Dataset to contain the exact full validation set; sampling, sharding, or persistent Spark file distribution would change semantics or leak large files.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Size the validation ingest listener backlog to the validation DataFrame partition count and add a deterministic regression that captures the requested backlog before bind.

## Prompting Intent
Resolve the exact-head Copilot finding on PR microsoft#2664 that a fixed backlog of 50 can reject simultaneous validation-partition connections when Spark schedules more than 50 ingest tasks, while preserving bounded transfer concurrency and construction cleanup.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Copilot review thread: microsoft#2664 (comment)

## Rationale
The accept loop intentionally processes at most eight transfers concurrently, so additional simultaneous connections wait in the operating-system accept queue. Passing the exact validation partition count avoids an artificial 50-connection bottleneck; the socket factory still clamps small values to the existing default and the operating system retains authority over its maximum backlog.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Project only the validation indicator column before collecting the one-row null-existence probe, and extend the public estimator regression with a feature vector larger than the configured driver result limit.

## Prompting Intent
Resolve the suppressed exact-head Copilot finding on PR microsoft#2664 that null validation-indicator detection could still return a complete wide feature row to the driver and recreate the driver-memory failure this PR removes.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Suppressed Copilot review on head 303fb96

## Rationale
The null check needs only existence, not any training columns. Selecting the boolean indicator before `head(1)` preserves the explicit null rejection semantics while bounding the returned row independently of feature width. The regression uses deterministic one-megabyte dense vectors under `spark.driver.maxResultSize=256k`; it fails before the projection and returns the intended validation error after it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Acquire the Spark serializer instance before opening the executor validation socket and add a deterministic lifecycle regression proving serializer construction failure cannot initiate a server connection.

## Prompting Intent
Resolve the suppressed exact-head Copilot finding on PR microsoft#2664 that executor iterator initialization opened its socket and input stream before serializer construction, allowing a serializer initialization failure to leak both resources.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Suppressed Copilot review on head b469be2

## Rationale
Serializer construction does not depend on the validation connection, so acquiring it before any network resource is the smallest and safest ownership ordering. Existing socket setup already closes the socket if authentication or input construction fails, and initial row decoding already closes both resources on failure. The regression listens on an ephemeral port and proves a synthetic serializer construction failure is preserved without any connection attempt.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Cap validation ingest accept polling at the configured socket timeout and add a deterministic construction-path regression for a 250-millisecond timeout.

## Prompting Intent
Resolve the suppressed exact-head Copilot finding on PR microsoft#2664 that the fixed one-second accept polling interval could delay configured subsecond ingest timeouts by up to one second.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Suppressed Copilot review on head 00330a4

## Rationale
The ingest accept loop still needs periodic wakeups for completion and shutdown, but its poll must never exceed the user-configured timeout. Taking the minimum of one second and the already validated socket timeout preserves the existing low-overhead polling for ordinary values and exact subsecond behavior without adding another timer or thread.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Resize the public LightGBM validation regression from 16,000 moderately sparse rows across eight tasks to 256 wider sparse rows across four tasks, and lower the driver result limit to retain a deterministic pre-fix failure.

## Prompting Intent
Address Azure build 231779254, where coverage instrumentation made the validation regression exceed the lightgbm1 job cap, without weakening its proof that validation rows are not collected on the driver or changing production behavior.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Failed Azure build: https://msdata.visualstudio.com/b9b2accc-2d1c-45b3-9d24-0eb5d78cc47f/_build/results?buildId=231779254

## Rationale
The smaller dataset retains exact estimator, schema, persistence, copy, sparse-vector, scoring, and cleanup coverage while reducing native training and complete validation transfer work by more than an order of magnitude. Deterministically varied nonzero values prevent task-result compression from hiding the old collect path: with the legacy collect behavior each partition returns about 156 KiB and fails the 128 KiB driver limit, while the streamed implementation passes. No production logic changed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Make the stalled-client test output treat interruption after socket closure as expected cancellation and discard subsequent synthetic writes, eliminating uncaught executor-thread errors without changing production code.

## Prompting Intent
Address coverage evidence from PR microsoft#2664 showing that the passing stalled-client lifecycle test emitted an uncaught Error wrapping InterruptedException after ValidationDataServer.close called shutdownNow.

## Linked Sources
- Pull request: microsoft#2664
- Azure build: https://msdata.visualstudio.com/b9b2accc-2d1c-45b3-9d24-0eb5d78cc47f/_build/results?buildId=231799793

## Rationale
The synthetic OutputStream exists only to block until the server closes its tracked socket. Once closure is observed, an interrupt from executor shutdown is expected and the test double can complete normally. An interrupt before socket closure is still rethrown, preserving the ordering assertion. Re-interrupting the terminating synthetic worker caused expected-disconnect logging to fail or delay executor termination under isolated coverage and Spark 4.1 runs, so the consumed cancellation is intentionally not restored.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
Fall back to the JVM temporary directory when the preferred working-directory validation spool cannot be created, preserve both construction failures, and make lifecycle tests deterministic under coverage.

## Prompting Intent
Resolve the exact-head suppressed review finding on PR microsoft#2664 without changing validation semantics or adding production complexity beyond an exception-safe spool-location fallback. Keep stalled-client cancellation clean and ensure the short listener polling timeout does not become a brittle client-read timeout in coverage runs.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Exact-head Copilot review on 61013c2

## Rationale
The working directory remains preferred so existing disk placement is unchanged when writable. Files.createTempDirectory provides atomic unique creation, while the standard JVM temporary location is used only after a non-fatal preferred-location failure. If both locations fail, the fallback failure remains primary and the preferred failure is suppressed for diagnosis. The idle-timeout test now separates the intended 10 ms listener poll from client reads, avoiding coverage-only timeouts without changing server behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
## Summary
In bulk single-dataset mode, stream the complete validation spool only to the active task on each executor while helper tasks satisfy shared synchronization without downloading duplicate validation data.

## Prompting Intent
Resolve the exact-head suppressed review finding on PR microsoft#2664 that helper-only bulk tasks unnecessarily consumed full validation transfers. Preserve exact validation semantics, keep non-single-dataset mode unchanged, avoid new unbounded resources, and prove the public estimator path on Spark 3.5 and Spark 4.1.

## Linked Sources
- Issue: microsoft#2294
- Pull request: microsoft#2664
- Exact-head Copilot review on 263da87

## Rationale
Single-dataset mode creates one native training and validation Dataset per executor, so only the active executor task needs the complete validation stream. Helpers still decrement the existing validation preparation latch so the active task cannot hang. Non-single-dataset bulk mode continues transferring the complete validation set to every training task, preserving native semantics. A direct policy assertion is paired with a public bulk-estimator regression for both single- and per-task Dataset modes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 19, 2026 10:52
@ranadeepsingh
Rana Singh (ranadeepsingh) force-pushed the copilot/issue-2294-validation-data-scaling branch from 85ec1fc to 395bd9a Compare August 19, 2026 10:52
@ranadeepsingh

Copy link
Copy Markdown
Collaborator Author

Rebased exact six-file patch onto origin/master 9f152c2ef78d9975ff4f855e1d05b85cb358563e; new head 395bd9aa2c899a836bc5cbcb762b9fec47b1ef79. No conflicts. All 20 commits matched one-for-one in git range-diff, and all six changed files are identical to pre-rebase head 85ec1fc111e6676ea7f29fd57cc5632f5ef41610. JDK 11 lightgbm/Test/compile, 4/4 estimator tests, the stalled-client lifecycle regression, and git diff --check passed. Master was fetched again immediately before the --force-with-lease push and was unchanged. #2662 remains open and was not incorporated.

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: 6/6 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

Copy link
Copy Markdown
Collaborator Author

Rebased-head audit complete: Copilot review at 395bd9aa2c899a836bc5cbcb762b9fec47b1ef79 reviewed all 6 files with 0 generated findings and no suppressed section; readiness reports 0 unresolved threads and 0 suppressed findings for head. New Azure build 231829385 is inProgress, reason=pullRequest, validating refs/pull/2664/merge with pr.sourceSha=395bd9aa2c899a836bc5cbcb762b9fec47b1ef79. Final target recheck remains origin/master 9f152c2ef78d9975ff4f855e1d05b85cb358563e (20 ahead, 0 behind).

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] OutOfMemorySparkException only when including a validationIndicatorCol - LightGBMClassifier

2 participants