Skip to content

Give transfer/sync-issues independent source/target concurrency and timeout - #553

Merged
okorach-sonar merged 5 commits into
mainfrom
fix/528-source-target-concurrency-timeout
Sep 1, 2026
Merged

Give transfer/sync-issues independent source/target concurrency and timeout#553
okorach-sonar merged 5 commits into
mainfrom
fix/528-source-target-concurrency-timeout

Conversation

@okorach-sonar

Copy link
Copy Markdown
Contributor

Summary

Fixes #528transfer and sync-issues now resolve concurrency/timeout independently for source (SonarQube Server) and target (SonarQube Cloud), instead of silently collapsing the two into one shared value.

extract and migrate already resolved these correctly per side (issue #266's unified config shape), each with its own semaphore and HTTP client. The bug was confined to transfer/sync-issues, which load the config file through both extract.LoadExtractConfigFile (source-resolved) and migrate.LoadMigrateConfigFile (target-resolved) but then collapsed the two already-correct values into one field:

switch {
case extractCfg.Concurrency != 0:
    cfg.concurrency = extractCfg.Concurrency   // arbitrarily prefers source
case migrateCfg.Concurrency != 0:
    cfg.concurrency = migrateCfg.Concurrency
}
cfg.timeout = extractCfg.Timeout   // target.timeout always silently dropped

So source.timeout: 30, target.timeout: 90 in a config file used to apply 30 to both phases — permanently discarding the target value — even though the documented example config (examples/config.unified.example.json) and transfer's own --help text have shown independent per-side values since #266.

  • Split transferConfig/syncIssuesConfig's concurrency/timeout fields into source*/target* pairs.
  • loadTransferFileDefaults/loadSyncIssuesFileDefaults: assign each loader's resolved value straight through instead of collapsing.
  • --concurrency/--timeout on the CLI now set both sides at once, matching the issue's spec that the config file is the only way to differ them for these two commands.
  • Each phase (extract.RunExtract, migrate.RunMigrate/RunSyncIssues) now gets its own side's values.

Test plan

  • go build ./..., go vet ./... clean
  • go test ./... -count=1 — full suite green
  • Updated TestResolveTransferConfig_UnifiedConfigShape / TestResolveSyncIssuesConfig_UnifiedConfigShape for the renamed fields
  • Updated TestResolveTransferConfig_CLIOverridesConfig / TestResolveSyncIssuesConfig_CLIOverridesConfig to assert --concurrency/--timeout set both source and target
  • Added TestResolveTransferConfig_SourceAndTargetConcurrencyTimeoutDiffer and TestResolveSyncIssuesConfig_SourceAndTargetConcurrencyTimeoutDiffer — regression tests proving distinct per-side config-file values resolve correctly (the scenario the old tests never covered)
  • Manual check of transfer --help output — flag descriptions still read correctly

…ncy and timeout (#528)

The network path from the migration host to the source SonarQube
Server can differ significantly from the path to the target
SonarQube Cloud, so a single shared concurrency/timeout doesn't fit
both sides. extract and migrate already resolve concurrency/timeout
independently per side via the unified source/target config-file
shape (#266) and use fully separate semaphores and HTTP clients.

transfer and sync-issues had the same bug: both load the config file
through both extract's and migrate's own loaders (each already
resolving its side correctly), but then collapsed the two
independently-resolved values into one shared field — always
discarding target.timeout in favor of source.timeout, and
arbitrarily preferring one side's concurrency when both were set.

Split transferConfig/syncIssuesConfig's concurrency/timeout into
source*/target* pairs, assign the two loaders' resolved values
straight through instead of collapsing them, and route each phase's
API config to its own pair. --concurrency/--timeout on the CLI now
set both sides at once (per the issue: the config file is the only
way to differ them for these two commands, since they talk to both
sides in one invocation).
@okorach-sonar
okorach-sonar requested a review from a team as a code owner September 1, 2026 06:57
Comment thread go/cmd/transfer.go Outdated
Comment thread go/cmd/transfer.go Outdated
SonarCloud's PR scan failed the quality gate on new_duplicated_lines_density
(39.7%, threshold 3%): the "--concurrency/--timeout set both source and
target" block was copy-pasted near-verbatim between transfer.go and
sync_issues.go. Extracted a shared applyFlagIntBothSides helper (next to
the existing applyFlagString/applyFlagInt/applyFlagBool) and use it from
both commands instead.
gitar-bot and others added 2 commits September 1, 2026 07:24
… clarify flag help text

Co-authored-by: Olivier K <161828284+okorach-sonar@users.noreply.github.com>
Gitar's auto-applied fix for "preserve per-side concurrency/timeout
with single-side fallback" pasted an identical 12-line block into both
transfer.go and sync_issues.go, spiking new_duplicated_lines_density
to 53.3% (threshold 3%). Extracted the bidirectional fallback into a
shared fallbackToOtherSide helper next to applyFlagIntBothSides, used
by both loadTransferFileDefaults and loadSyncIssuesFileDefaults.

Also brought sync-issues's --concurrency/--timeout flag help text in
line with transfer's clarified wording, and added a regression test
per command for the single-side-fallback behavior itself.
Comment thread go/cmd/transfer.go
Comment on lines +190 to +191
f.Int(flagConcurrency, 0, "Max concurrent requests, applied to both source and target (default: 25). Use source.concurrency / target.concurrency in the config file to set them independently.")
f.Int(flagTimeout, 0, "HTTP request timeout in seconds, applied to both source and target (default: 60). Use source.timeout / target.timeout in the config file to set them independently.")

@gitar-bot gitar-bot Bot Sep 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Quality: Cross-side concurrency/timeout fill contradicts the docs it points to

The new fallbackToOtherSide makes a value set on one side leak to the other: a transfer config with only target.timeout: 30 (no top-level, nothing under source) now applies 30 to the extract phase too, and only source.concurrency: 50 now drives the SonarQube Cloud phase at 50 instead of the documented default. That directly contradicts docs/ADVANCED-CONFIG.md, which states source.concurrency/source.timeout override "for extract calls" and target.* "for migrate / reset calls", and contradicts the help text added in this same commit ("Use source.concurrency / target.concurrency in the config file to set them independently") — with the fallback, one side can never be set independently while the other keeps its default. docs/TRANSFER.md:222-223 and docs/SYNC-ISSUES.md:117-118 also still document only a single top-level concurrency/timeout field for these two commands, so the per-side fields the new help text tells operators to use are undocumented there. Update the docs (and the flag help) to state the cross-fill rule explicitly, or drop the fallback so an unset side uses the package default.

Document the cross-side fill in docs/ADVANCED-CONFIG.md (mirror the wording in the two target. rows) and add source./target. concurrency+timeout rows to docs/TRANSFER.md and docs/SYNC-ISSUES.md.:*

| `source.concurrency` | `--concurrency` | top-level | No | Override top-level concurrency for extract calls. On `transfer`/`sync-issues`, if only one side sets it, the other side borrows that value (#528). |
| `source.timeout` | `--timeout` | top-level | No | Override top-level timeout for extract calls. Same single-side borrowing rule as above on `transfer`/`sync-issues`. |

Was this helpful? React with 👍 / 👎

Comment thread go/cmd/transfer.go
Comment on lines +261 to +268
func fallbackToOtherSide(a, b *int) {
if *a == 0 {
*a = *b
}
if *b == 0 {
*b = *a
}
}

@gitar-bot gitar-bot Bot Sep 1, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Quality: New fallback tests cover only one of the helper's two branches

Both new tests set concurrency/timeout only under target and assert only cfg.sourceConcurrency/cfg.sourceTimeout, so the if *b == 0 { *b = *a } branch of fallbackToOtherSide (source-only config → target borrows) is never executed, and neither test asserts that the explicitly-set side survives the call (a fix that overwrote the set side with the zero one would still pass). Add the mirrored source-only case and assert both sides in each test.

Assert both sides and add the mirrored source-only case so the second branch of fallbackToOtherSide is exercised.:

if cfg.sourceConcurrency != 5 {
	t.Errorf("sourceConcurrency: got %d, want 5 (borrowed from target)", cfg.sourceConcurrency)
}
if cfg.targetConcurrency != 5 {
	t.Errorf("targetConcurrency: got %d, want 5 (explicitly set)", cfg.targetConcurrency)
}
if cfg.sourceTimeout != 30 {
	t.Errorf("sourceTimeout: got %d, want 30 (borrowed from target)", cfg.sourceTimeout)
}
if cfg.targetTimeout != 30 {
	t.Errorf("targetTimeout: got %d, want 30 (explicitly set)", cfg.targetTimeout)
}
// plus a mirrored sub-test with concurrency/timeout only under "source",
// asserting target borrows them.

Was this helpful? React with 👍 / 👎

…function

new_duplicated_lines_density was still at 32.9% after extracting
fallbackToOtherSide: the 6-line "resolve source/target concurrency and
timeout" sequence around each fallbackToOtherSide call was still
copy-pasted identically into both loadTransferFileDefaults and
loadSyncIssuesFileDefaults. Folded the whole sequence into a single
resolveSourceTargetRates(extractCfg, migrateCfg) function next to
fallbackToOtherSide, called with one line from each command.
@gitar-bot

gitar-bot Bot commented Sep 1, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 2 resolved / 4 findings

Splits transfer and sync-issues concurrency/timeout into independent source/target fields, but the new cross-side fallback contradicts the documented per-side semantics and creates undocumented behavior where an unset side borrows from the other. Remove the fallback so each side uses its configured value or the package default, then update docs/TRANSFER.md and docs/SYNC-ISSUES.md to document the per-side fields. Additionally, the fallback tests cover only one code path and don't assert that explicitly-set values survive — add source-only test cases and assertions for both sides.

⚠️ Quality: Cross-side concurrency/timeout fill contradicts the docs it points to

📄 go/cmd/transfer.go:190-191 📄 go/cmd/transfer.go:254-268 📄 go/cmd/transfer.go:335-344 📄 go/cmd/sync_issues.go:84-85 📄 go/cmd/sync_issues.go:144-153

The new fallbackToOtherSide makes a value set on one side leak to the other: a transfer config with only target.timeout: 30 (no top-level, nothing under source) now applies 30 to the extract phase too, and only source.concurrency: 50 now drives the SonarQube Cloud phase at 50 instead of the documented default. That directly contradicts docs/ADVANCED-CONFIG.md, which states source.concurrency/source.timeout override "for extract calls" and target.* "for migrate / reset calls", and contradicts the help text added in this same commit ("Use source.concurrency / target.concurrency in the config file to set them independently") — with the fallback, one side can never be set independently while the other keeps its default. docs/TRANSFER.md:222-223 and docs/SYNC-ISSUES.md:117-118 also still document only a single top-level concurrency/timeout field for these two commands, so the per-side fields the new help text tells operators to use are undocumented there. Update the docs (and the flag help) to state the cross-fill rule explicitly, or drop the fallback so an unset side uses the package default.

Document the cross-side fill in docs/ADVANCED-CONFIG.md (mirror the wording in the two target.* rows) and add source.*/target.* concurrency+timeout rows to docs/TRANSFER.md and docs/SYNC-ISSUES.md.
| `source.concurrency` | `--concurrency` | top-level | No | Override top-level concurrency for extract calls. On `transfer`/`sync-issues`, if only one side sets it, the other side borrows that value (#528). |
| `source.timeout` | `--timeout` | top-level | No | Override top-level timeout for extract calls. Same single-side borrowing rule as above on `transfer`/`sync-issues`. |
💡 Quality: New fallback tests cover only one of the helper's two branches

📄 go/cmd/transfer.go:261-268 📄 go/cmd/transfer_test.go:286-300 📄 go/cmd/sync_issues_test.go:212-226

Both new tests set concurrency/timeout only under target and assert only cfg.sourceConcurrency/cfg.sourceTimeout, so the if *b == 0 { *b = *a } branch of fallbackToOtherSide (source-only config → target borrows) is never executed, and neither test asserts that the explicitly-set side survives the call (a fix that overwrote the set side with the zero one would still pass). Add the mirrored source-only case and assert both sides in each test.

Assert both sides and add the mirrored source-only case so the second branch of fallbackToOtherSide is exercised.
if cfg.sourceConcurrency != 5 {
	t.Errorf("sourceConcurrency: got %d, want 5 (borrowed from target)", cfg.sourceConcurrency)
}
if cfg.targetConcurrency != 5 {
	t.Errorf("targetConcurrency: got %d, want 5 (explicitly set)", cfg.targetConcurrency)
}
if cfg.sourceTimeout != 30 {
	t.Errorf("sourceTimeout: got %d, want 30 (borrowed from target)", cfg.sourceTimeout)
}
if cfg.targetTimeout != 30 {
	t.Errorf("targetTimeout: got %d, want 30 (explicitly set)", cfg.targetTimeout)
}
// plus a mirrored sub-test with concurrency/timeout only under "source",
// asserting target borrows them.
✅ 2 resolved
Bug: Removing cross-side fallback silently resets one side to defaults

📄 go/cmd/transfer.go:301-311 📄 go/cmd/sync_issues.go:144-150
The deleted switch/cfg.timeout = extractCfg.Timeout also acted as a cross-side fallback, which matters for the command-sectioned shape (shape 2) that loadTransferFileDefaults explicitly still supports: for {"extract":{"url":...,"concurrency":10,"timeout":300},"migrate":{"token":...}}, migrate.LoadMigrateConfigFile returns Concurrency=0/Timeout=0 (its shape-2 branch reads only the migrate section and ignores top-level values, config_file.go:239), so the migrate phase now silently drops from the operator's 300s timeout / concurrency 10 to the hardcoded defaults 60s / 25 (applyDefaults). The same happens in reverse for the extract phase when only the migrate section carries the values, and in sync-issues. Preserve a per-field fallback so a value configured on only one side still applies to the other, and add a command-sectioned-shape test (the new regression tests only cover the unified shape).

Quality: --concurrency/--timeout help text omits new both-sides semantics

📄 go/cmd/transfer.go:190-191 📄 go/cmd/transfer.go:347-358 📄 go/cmd/sync_issues.go:183-193
--concurrency/--timeout now write both source* and target* fields, but their flag descriptions still read only "(maps to concurrency)" / "(maps to timeout; default: 60)" with no indication that they set both sides and that the config file is the only way to give the sides different values — contradicting the PR description's claim that the existing --help text already "shows independent per-side values". Update the two flag descriptions in transfer's and sync-issues' init() so operators can discover the per-side config-file capability.

🤖 Prompt for agents
Code Review: Splits `transfer` and `sync-issues` concurrency/timeout into independent source/target fields, but the new cross-side fallback contradicts the documented per-side semantics and creates undocumented behavior where an unset side borrows from the other. Remove the fallback so each side uses its configured value or the package default, then update docs/TRANSFER.md and docs/SYNC-ISSUES.md to document the per-side fields. Additionally, the fallback tests cover only one code path and don't assert that explicitly-set values survive — add source-only test cases and assertions for both sides.

1. ⚠️ Quality: Cross-side concurrency/timeout fill contradicts the docs it points to
   Files: go/cmd/transfer.go:190-191, go/cmd/transfer.go:254-268, go/cmd/transfer.go:335-344, go/cmd/sync_issues.go:84-85, go/cmd/sync_issues.go:144-153

   The new `fallbackToOtherSide` makes a value set on one side leak to the other: a transfer config with only `target.timeout: 30` (no top-level, nothing under `source`) now applies 30 to the extract phase too, and only `source.concurrency: 50` now drives the SonarQube Cloud phase at 50 instead of the documented default. That directly contradicts docs/ADVANCED-CONFIG.md, which states `source.concurrency`/`source.timeout` override "for extract calls" and `target.*` "for migrate / reset calls", and contradicts the help text added in this same commit ("Use source.concurrency / target.concurrency in the config file to set them independently") — with the fallback, one side can never be set independently while the other keeps its default. docs/TRANSFER.md:222-223 and docs/SYNC-ISSUES.md:117-118 also still document only a single top-level `concurrency`/`timeout` field for these two commands, so the per-side fields the new help text tells operators to use are undocumented there. Update the docs (and the flag help) to state the cross-fill rule explicitly, or drop the fallback so an unset side uses the package default.

   Fix (Document the cross-side fill in docs/ADVANCED-CONFIG.md (mirror the wording in the two target.* rows) and add source.*/target.* concurrency+timeout rows to docs/TRANSFER.md and docs/SYNC-ISSUES.md.):
   | `source.concurrency` | `--concurrency` | top-level | No | Override top-level concurrency for extract calls. On `transfer`/`sync-issues`, if only one side sets it, the other side borrows that value (#528). |
   | `source.timeout` | `--timeout` | top-level | No | Override top-level timeout for extract calls. Same single-side borrowing rule as above on `transfer`/`sync-issues`. |

2. 💡 Quality: New fallback tests cover only one of the helper's two branches
   Files: go/cmd/transfer.go:261-268, go/cmd/transfer_test.go:286-300, go/cmd/sync_issues_test.go:212-226

   Both new tests set concurrency/timeout only under `target` and assert only `cfg.sourceConcurrency`/`cfg.sourceTimeout`, so the `if *b == 0 { *b = *a }` branch of `fallbackToOtherSide` (source-only config → target borrows) is never executed, and neither test asserts that the explicitly-set side survives the call (a fix that overwrote the set side with the zero one would still pass). Add the mirrored source-only case and assert both sides in each test.

   Fix (Assert both sides and add the mirrored source-only case so the second branch of fallbackToOtherSide is exercised.):
   if cfg.sourceConcurrency != 5 {
   	t.Errorf("sourceConcurrency: got %d, want 5 (borrowed from target)", cfg.sourceConcurrency)
   }
   if cfg.targetConcurrency != 5 {
   	t.Errorf("targetConcurrency: got %d, want 5 (explicitly set)", cfg.targetConcurrency)
   }
   if cfg.sourceTimeout != 30 {
   	t.Errorf("sourceTimeout: got %d, want 30 (borrowed from target)", cfg.sourceTimeout)
   }
   if cfg.targetTimeout != 30 {
   	t.Errorf("targetTimeout: got %d, want 30 (explicitly set)", cfg.targetTimeout)
   }
   // plus a mirrored sub-test with concurrency/timeout only under "source",
   // asserting target borrows them.

Implementation Status ✅ 4 of 4 objectives covered
#528 - 4 of 4 objectives covered

This PR implements independent source and target concurrency and timeout configurations for transfer and sync-issues, covering all specified objectives.

✅ 4 covered here
  • ✅ Allow concurrency and timeout parameters to be set at the top level, as well as independently for the source and target
  • ✅ Override default or top-level concurrency and timeout values with nested source or target section values in the config file
  • ✅ Automatically apply CLI --concurrency and --timeout parameters to the source SQS when running the extract command, and to the target SQC when running the migrate command
  • ✅ Automatically apply CLI --concurrency and --timeout parameters to both source SQS and target SQC when running the transfer or issue_sync command, while allowing different values via configuration
Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.
Unblock → Override a blocking verdict and allow merging.

Comment with these commands to change the behavior for this request:

Auto-apply Compact Unblock
gitar auto-apply:on         
gitar display:verbose         
gitar unblock         

Was this helpful? React with 👍 / 👎 | Gitar

@sonarqubecloud

sonarqubecloud Bot commented Sep 1, 2026

Copy link
Copy Markdown

@okorach-sonar
okorach-sonar merged commit 465a74f into main Sep 1, 2026
10 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.

Allow a different concurrency and timeout on API calls on the source and the target SonarQube

4 participants