Skip to content

optimising ground-control satellite state reconciliation by centralis…#449

Open
devlopharsh wants to merge 1 commit into
container-registry:mainfrom
devlopharsh:bug/centralize-satellite-state-reconciliation
Open

optimising ground-control satellite state reconciliation by centralis…#449
devlopharsh wants to merge 1 commit into
container-registry:mainfrom
devlopharsh:bug/centralize-satellite-state-reconciliation

Conversation

@devlopharsh
Copy link
Copy Markdown

@devlopharsh devlopharsh commented May 17, 2026

Description

Centralizes Ground Control satellite state reconciliation by moving repeated state rebuild/publish logic into a shared helper.

Changes

  • added a shared satellite state reconciliation path
  • replaced duplicated inline state reconstruction in config, group, and satellite handlers
  • kept existing API and behavior unchanged.

Testing

  • go test ./internal/server/...

Additional context

Summary by CodeRabbit

  • Refactor
    • Improved satellite management by consolidating state-handling procedures across configuration updates, satellite registration, group associations, and maintenance operations. This centralization eliminates code duplication, ensures consistent state management practices throughout the system, and enhances overall code maintainability without changing any user-visible features.

Review Change Stack

…ation

Signed-off-by: devlopharsh <harsh237hk@gmail.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 17, 2026

📝 Walkthrough

Walkthrough

This PR consolidates duplicate satellite state artifact reconstruction logic across multiple handlers into two centralized helpers: buildSatelliteStateDetails (which assembles the satellite, groups, states, and config) and reconcileSatelliteState (which publishes the artifact). Config assignment, group deletion, satellite registration, ZTR, and group membership handlers are updated to use these helpers instead of inline state-building logic.

Changes

Satellite State Reconciliation Refactoring

Layer / File(s) Summary
New state reconciliation helpers
ground-control/internal/server/helpers.go
Introduces satelliteStateDetails struct, buildSatelliteStateDetails (loads satellite, groups, derives group states, aggregates projects, fetches config), and reconcileSatelliteState (calls builder then updates artifact). Existing helpers (config fetch, auth parsing, nullable conversions, heartbeat normalization) retain their logic with formatting normalization.
Config handler state updates
ground-control/internal/server/config_handlers.go
setSatelliteConfig is refactored to call reconcileSatelliteState(r.Context(), q, sat.ID) after setting the config, replacing prior inline group/state/artifact assembly. Other config handlers (createConfigHandler, updateConfigHandler, listConfigsHandler, getConfigHandler, deleteConfigHandler) keep existing behavior.
Group handler state updates
ground-control/internal/server/group_handlers.go
In deleteGroupHandler, per-satellite cleanup replaces manual group/state assembly with buildSatelliteStateDetails, updates robot permissions from details.Projects, and refreshes state artifact via reconcileSatelliteState instead of prior fetch-satellite + fetch-config + artifact-create sequence.
Satellite registration and ZTR state updates
ground-control/internal/server/satellite_handlers.go
In registerSatelliteHandler, state-artifact creation is replaced with reconcileSatelliteState after group association. In ztrHandler, pre-reconciliation flow (list groups, compute states, fetch config, create artifact) is replaced by early GetSatellite followed by reconcileSatelliteState; Harbor-available branch similarly uses reconcileSatelliteState.
Satellite group membership state updates
ground-control/internal/server/satellite_handlers.go
In addSatelliteToGroup and removeSatelliteFromGroup, manual satellite group/state aggregation is replaced by buildSatelliteStateDetails, robot projects are updated from details.Projects, and state artifacts are refreshed via reconcileSatelliteState instead of manual group-state assembly and CreateOrUpdateSatStateArtifact calls.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

golang, enhancement

Suggested reviewers

  • Vad1mo
  • bupd
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is truncated and incomplete, making it vague about the specific optimization being performed. Complete the title with a clear phrase such as 'optimizing ground-control satellite state reconciliation by centralizing rebuild logic' to convey the full intent.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The PR description covers all required sections, clearly references the linked issue, and explains the main changes and testing approach.
Linked Issues check ✅ Passed The PR successfully implements all proposed objectives from issue #447: introduces centralized helpers (buildSatelliteStateDetails and reconcileSatelliteState), replaces duplicated inline state reconstruction across config, group, and satellite handlers, and maintains existing API behavior.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the stated objectives in issue #447; no out-of-scope modifications were detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codacy-production
Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics -5 complexity

Metric Results
Complexity -5

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
ground-control/internal/server/helpers.go (1)

229-245: ⚡ Quick win

Avoid loading each group twice while building reconciliation details.

getGroupStates already does a GetGroupByID per group, and this loop immediately repeats the same lookup to collect Projects. In the add/remove/delete flows that extra N+1 runs inside an open transaction for every satellite, so folding state/project aggregation into one pass would cut the new reconciliation overhead noticeably.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ground-control/internal/server/helpers.go` around lines 229 - 245, The code
is doing duplicate GetGroupByID calls: getGroupStates(ctx, groupList, q) already
queries each group, then the subsequent loop re-calls q.GetGroupByID for every
group to collect grp.Projects, causing N+1 overhead inside the transaction. Fix
by folding project aggregation into the single pass: modify getGroupStates (or
its return type) to also return each group's Projects (e.g., return a map or
include Projects in the returned groupStates), then use that returned data
instead of calling q.GetGroupByID again in the loop that builds projects; update
callers that use getGroupStates accordingly to avoid the second q.GetGroupByID
calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ground-control/internal/server/helpers.go`:
- Around line 260-267: reconcileSatelliteState currently calls
CreateOrUpdateSatStateArtifact (which mutates Harbor) before the DB transaction
is committed, exposing non-durable state if the transaction rolls back; change
the flow so that artifact publication happens only after a successful commit —
e.g., have reconcileSatelliteState return the prepared details (or enqueue an
outbox record) instead of calling CreateOrUpdateSatStateArtifact directly, then
invoke CreateOrUpdateSatStateArtifact (or a worker that processes the outbox)
after tx.Commit() completes; update callers that expect reconcileSatelliteState
to no longer side-effect Harbor and use the new after-commit/outbox handoff to
perform the Harbor mutation reliably.
- Around line 73-84: The helper currently only returns an error when
harbor.ListReplication returns an error, but ignores the case where err==nil and
replications is empty, allowing unknown groups; change the logic in the block
using harbor.ListReplication (the replications variable from
harbor.ListReplication(ctx, harbor.ListParams{Q: fmt.Sprintf("name=%s",
groupName)})) so that you first handle the err != nil case and return an
AppError on failure, and then separately check if len(replications) < 1 and
return an AppError stating the group does not exist (using the same AppError
shape used elsewhere), ensuring the function refuses empty results even when err
== nil.

---

Nitpick comments:
In `@ground-control/internal/server/helpers.go`:
- Around line 229-245: The code is doing duplicate GetGroupByID calls:
getGroupStates(ctx, groupList, q) already queries each group, then the
subsequent loop re-calls q.GetGroupByID for every group to collect grp.Projects,
causing N+1 overhead inside the transaction. Fix by folding project aggregation
into the single pass: modify getGroupStates (or its return type) to also return
each group's Projects (e.g., return a map or include Projects in the returned
groupStates), then use that returned data instead of calling q.GetGroupByID
again in the loop that builds projects; update callers that use getGroupStates
accordingly to avoid the second q.GetGroupByID calls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: de2f06ad-fc08-4e62-a5e3-44b6719cbbb7

📥 Commits

Reviewing files that changed from the base of the PR and between aef6e4e and 03c8efa.

📒 Files selected for processing (4)
  • ground-control/internal/server/config_handlers.go
  • ground-control/internal/server/group_handlers.go
  • ground-control/internal/server/helpers.go
  • ground-control/internal/server/satellite_handlers.go

Comment on lines +73 to +84
replications, err := harbor.ListReplication(ctx, harbor.ListParams{
Q: fmt.Sprintf("name=%s", groupName),
})
if len(replications) < 1 {
if err != nil {
log.Println(err)
return nil, &AppError{
Message: fmt.Sprintf("Error: Group Name: %s, does not exist in replication, Please give a Valid Group Name", groupName),
Code: http.StatusBadRequest,
}
}
}
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject groups missing from Harbor even when the lookup itself succeeds.

An empty ListReplication result with err == nil currently falls through and still links the satellite to the DB group. That accepts groups that are not declared in Harbor, which breaks the validation this helper is trying to enforce.

Proposed fix
 			replications, err := harbor.ListReplication(ctx, harbor.ListParams{
 				Q: fmt.Sprintf("name=%s", groupName),
 			})
-			if len(replications) < 1 {
-				if err != nil {
-					log.Println(err)
-					return nil, &AppError{
-						Message: fmt.Sprintf("Error: Group Name: %s, does not exist in replication, Please give a Valid Group Name", groupName),
-						Code:    http.StatusBadRequest,
-					}
-				}
+			if err != nil {
+				log.Println(err)
+				return nil, &AppError{
+					Message: "Error: failed to validate group replication",
+					Code:    http.StatusBadGateway,
+				}
+			}
+			if len(replications) == 0 {
+				return nil, &AppError{
+					Message: fmt.Sprintf("Error: Group Name: %s, does not exist in replication, Please give a Valid Group Name", groupName),
+					Code:    http.StatusBadRequest,
+				}
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
replications, err := harbor.ListReplication(ctx, harbor.ListParams{
Q: fmt.Sprintf("name=%s", groupName),
})
if len(replications) < 1 {
if err != nil {
log.Println(err)
return nil, &AppError{
Message: fmt.Sprintf("Error: Group Name: %s, does not exist in replication, Please give a Valid Group Name", groupName),
Code: http.StatusBadRequest,
}
}
}
replications, err := harbor.ListReplication(ctx, harbor.ListParams{
Q: fmt.Sprintf("name=%s", groupName),
})
if err != nil {
log.Println(err)
return nil, &AppError{
Message: "Error: failed to validate group replication",
Code: http.StatusBadGateway,
}
}
if len(replications) == 0 {
return nil, &AppError{
Message: fmt.Sprintf("Error: Group Name: %s, does not exist in replication, Please give a Valid Group Name", groupName),
Code: http.StatusBadRequest,
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ground-control/internal/server/helpers.go` around lines 73 - 84, The helper
currently only returns an error when harbor.ListReplication returns an error,
but ignores the case where err==nil and replications is empty, allowing unknown
groups; change the logic in the block using harbor.ListReplication (the
replications variable from harbor.ListReplication(ctx, harbor.ListParams{Q:
fmt.Sprintf("name=%s", groupName)})) so that you first handle the err != nil
case and return an AppError on failure, and then separately check if
len(replications) < 1 and return an AppError stating the group does not exist
(using the same AppError shape used elsewhere), ensuring the function refuses
empty results even when err == nil.

Comment on lines +260 to +267
func reconcileSatelliteState(ctx context.Context, q *database.Queries, satelliteID int32) error {
details, err := buildSatelliteStateDetails(ctx, q, satelliteID)
if err != nil {
log.Printf("Error: Failed to fetch satellite config: %v", err)
return database.Config{}, &AppError{
Message: "Error: Failed to fetch satellite config",
Code: http.StatusInternalServerError,
}
return err
}

configObject, err := dbQueries.GetConfigByID(ctx, satelliteConfig.ConfigID)
if err != nil {
log.Printf("Error: Failed to fetch satellite config: %v", err)
return database.Config{}, &AppError{
Message: "Error: Failed to fetch satellite config",
if err := utils.CreateOrUpdateSatStateArtifact(ctx, details.Satellite.Name, details.GroupStates, details.Config.ConfigName); err != nil {
log.Printf("failed to update satellite state artifact for %s: %v", details.Satellite.Name, err)
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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don’t publish Harbor state before the DB transaction is durable.

CreateOrUpdateSatStateArtifact mutates Harbor immediately, but all of the new callers invoke this helper before tx.Commit(). If the transaction later rolls back or the commit fails, Harbor exposes a satellite state that never became durable in Postgres. This centralized helper is the right place to move to an after-commit or outbox-style handoff.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ground-control/internal/server/helpers.go` around lines 260 - 267,
reconcileSatelliteState currently calls CreateOrUpdateSatStateArtifact (which
mutates Harbor) before the DB transaction is committed, exposing non-durable
state if the transaction rolls back; change the flow so that artifact
publication happens only after a successful commit — e.g., have
reconcileSatelliteState return the prepared details (or enqueue an outbox
record) instead of calling CreateOrUpdateSatStateArtifact directly, then invoke
CreateOrUpdateSatStateArtifact (or a worker that processes the outbox) after
tx.Commit() completes; update callers that expect reconcileSatelliteState to no
longer side-effect Harbor and use the new after-commit/outbox handoff to perform
the Harbor mutation reliably.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="ground-control/internal/server/satellite_handlers.go">

<violation number="1" location="ground-control/internal/server/satellite_handlers.go:1109">
P2: Duplicate state-rebuild work in add/remove group handlers: `buildSatelliteStateDetails` is called to get `details.Projects`, then `reconcileSatelliteState` is called which internally rebuilds the same state again, causing all DB queries to run twice within one request path.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Re-trigger cubic

// Update the state artifact to also track the new group state artifact
err = utils.CreateOrUpdateSatStateArtifact(r.Context(), sat.Name, groupStates, configObject.ConfigName)
if err != nil {
if err := reconcileSatelliteState(r.Context(), q, sat.ID); err != nil {
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.

P2: Duplicate state-rebuild work in add/remove group handlers: buildSatelliteStateDetails is called to get details.Projects, then reconcileSatelliteState is called which internally rebuilds the same state again, causing all DB queries to run twice within one request path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At ground-control/internal/server/satellite_handlers.go, line 1109:

<comment>Duplicate state-rebuild work in add/remove group handlers: `buildSatelliteStateDetails` is called to get `details.Projects`, then `reconcileSatelliteState` is called which internally rebuilds the same state again, causing all DB queries to run twice within one request path.</comment>

<file context>
@@ -1197,9 +1106,7 @@ func (s *Server) addSatelliteToGroup(w http.ResponseWriter, r *http.Request) {
-	// Update the state artifact to also track the new group state artifact
-	err = utils.CreateOrUpdateSatStateArtifact(r.Context(), sat.Name, groupStates, configObject.ConfigName)
-	if err != nil {
+	if err := reconcileSatelliteState(r.Context(), q, sat.ID); err != nil {
 		log.Printf("Error: Failed to update satellite state artifact: %v", err)
 		HandleAppError(w, err)
</file context>

Copy link
Copy Markdown

@shri771 shri771 left a comment

Choose a reason for hiding this comment

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

Left a suggestion. Also, the AI Bot suggestion seems reasonable to me.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In the robot-account paths, buildSatelliteStateDetails is called for details. Projects and then reconcileSatelliteState runs right after, which rebuilds the same details internally. Could we add a variant of reconcileSatelliteState next to the existing one in helpers.go:260 that accepts a prebuilt details value and use it at those three sites?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Centralize satellite state artifact reconciliation in Ground Control to avoid duplicated reconstruction of ground states

2 participants