Skip to content

Conversation

@adityachoudhari26
Copy link
Contributor

@adityachoudhari26 adityachoudhari26 commented Oct 16, 2025

Summary by CodeRabbit

  • Bug Fixes

    • Improved error propagation and handling across event processing and API handlers to avoid silent failures and return clear error responses.
  • Refactor

    • Standardized workspace initialization and retrieval flows for more consistent behavior when creating or loading workspaces.
  • New Features

    • Added a persistent workspace existence check to validate workspace presence before load.
  • Tests

    • Added coverage to verify workspace existence detection.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 16, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

ListenAndRoute now returns an error and propagates handler failures; workspace initialization was moved to workspace.PopulateWorkspaceWithInitialState and persisted via workspace.Set; GetWorkspace signature changed to return (*workspace.Workspace, error) and error handling was added across several HTTP endpoints. (47 words)

Changes

Cohort / File(s) Summary
Event Handler Error Flow
apps/workspace-engine/pkg/events/handler/handler.go
ListenAndRoute now returns error; handler invocation records span errors and returns formatted errors; initialization switched to workspace.PopulateWorkspaceWithInitialState; new workspace.Set(rawEvent.WorkspaceID, ws) call; removed duplicated post-flush error block.
Workspace Population API
apps/workspace-engine/pkg/workspace/populate_workspace.go
Moved/renamed loadWorkspaceWithInitialStatePopulateWorkspaceWithInitialState and changed package/type usage to workspace package and *Workspace.
Workspace Utilities & DB
apps/workspace-engine/pkg/server/openapi/utils/utils.go, apps/workspace-engine/pkg/db/workspaces.go, apps/workspace-engine/pkg/db/workspaces_test.go
GetWorkspace signature changed to (*workspace.Workspace, error); added db.WorkspaceExists check and workspace.Set usage on initial population; added WORKSPACE_EXISTS_QUERY and WorkspaceExists(ctx, workspaceId) (bool, error) plus unit test.
Server Endpoint Error Handling
apps/workspace-engine/pkg/server/openapi/deployments/server.go, apps/workspace-engine/pkg/server/openapi/environments/server.go, apps/workspace-engine/pkg/server/openapi/policies/policies.go, apps/workspace-engine/pkg/server/openapi/releasetargets/releasetargets.go
Replaced nil-workspace checks with two-value GetWorkspace error handling; endpoints now return HTTP 500 on GetWorkspace errors and propagate descriptive error messages.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as HTTP / Event Listener
    participant Utils as GetWorkspace
    participant DB as DB
    participant Cache as Workspace Cache
    participant Pop as PopulateWorkspaceWithInitialState

    rect #F0FFF0
    Note over Caller,Utils: New flow — GetWorkspace returns (ws, err)
    Caller->>Utils: GetWorkspace(ctx, id)
    Utils->>DB: WorkspaceExists(id)?
    alt exists in DB
        DB-->>Utils: true
        Utils->>Cache: workspace.GetWorkspace(id)
        Cache-->>Utils: ws
        Utils-->>Caller: (ws, nil)
    else not in DB
        DB-->>Utils: false
        Utils->>Cache: workspace.New(id)
        Cache-->>Utils: ws
        Utils->>Pop: PopulateWorkspaceWithInitialState(ctx, ws)
        alt populate ok
            Pop-->>Utils: nil
            Utils->>Cache: workspace.Set(id, ws)
            Utils-->>Caller: (ws, nil)
        else populate failed
            Pop-->>Utils: err
            Utils-->>Caller: (nil, err)
        end
    end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • j7m4
  • jsbroks

Poem

🐰
I hopped through code to mend the flow,
Errors now surface, no more shadow,
Seeded workspaces set in cache,
Handlers tidy, spans attach,
A little rabbit cheers: hop, go!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The title "chore: maybe init workspace from server" refers to a real and significant aspect of the pull request's changes: the refactoring to initialize workspaces from the server side. The changes include moving workspace initialization logic to a public API (PopulateWorkspaceWithInitialState in the workspace package), updating utils.GetWorkspace to handle workspace creation, and modifying multiple server handlers to use the new pattern. However, the word "maybe" introduces vagueness and creates uncertainty about whether this is a definitive refactoring or still an exploratory/tentative change, which reduces clarity for teammates scanning the commit history. Remove the word "maybe" to make the title definitive and clear. Consider alternatives like "chore: init workspace from server" or "refactor: init workspace from server" that convey the same information without introducing uncertainty about whether the change is actually implemented or still pending. This will improve clarity for code history reviews.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 58659da and c114043.

📒 Files selected for processing (3)
  • apps/workspace-engine/pkg/db/workspaces.go (1 hunks)
  • apps/workspace-engine/pkg/db/workspaces_test.go (1 hunks)
  • apps/workspace-engine/pkg/server/openapi/utils/utils.go (1 hunks)

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.

Copy link
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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/workspace-engine/pkg/selector/resources.go (1)

35-87: Remove the unconditional stdout dumps.

Line 35 onwards now pretty-prints every selector/condition/match result to stdout using fmt.Printf. FilterResources sits on the request path, so this will emit verbose JSON for every call, bypass structured logging, overwhelm logs under load, and leak tenant-scoped payloads (workspace IDs, selector predicates) into shared stdout. Please drop these prints or gate them behind the existing logger at a debug level with an explicit feature flag before merging.

Apply this diff to remove the stdout logging:

-	if sel != nil {
-		// Try to marshal the resource selector to JSON for readability
-		if jsonBytes, err := json.MarshalIndent(sel, "", "  "); err == nil {
-			fmt.Printf("selector:\n%s\n", string(jsonBytes))
-		} else {
-			fmt.Printf("ResourceSelector for selector (marshal error: %v): %#v\n", err, sel)
-		}
-	} else {
-		fmt.Printf("ResourceSelector for selector: <nil>\n")
-	}
+	if sel == nil {
+		return map[string]*oapi.Resource{}, nil
+	}
 
-	if jsonBytes, err := json.MarshalIndent(jsonSelector, "", "  "); err == nil {
-		fmt.Printf("jsonSelector:\n%s\n", string(jsonBytes))
-	} else {
-		fmt.Printf("jsonSelector (marshal error: %v): %#v\n", err, jsonSelector)
-	}
-
 	if jsonSelector.Json == nil {
 		return map[string]*oapi.Resource{}, nil
 	}
 
-	if jsonBytes, err := json.MarshalIndent(unknownCondition, "", "  "); err == nil {
-		fmt.Printf("unknownCondition:\n%s\n", string(jsonBytes))
-	} else {
-		fmt.Printf("unknownCondition (marshal error: %v): %#v\n", err, unknownCondition)
-	}
-
 	selector, err := jsonselector.ConvertToSelector(ctx, unknownCondition)
 	if err != nil {
 		return nil, err
 	}
-
-	if jsonBytes, err := json.MarshalIndent(selector, "", "  "); err == nil {
-		fmt.Printf("selector:\n%s\n", string(jsonBytes))
-	} else {
-		fmt.Printf("selector (marshal error: %v): %#v\n", err, selector)
-	}
-
-	fmt.Printf("matchedResources: %d\n", len(matchedResources))
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 58f706a and a62a126.

📒 Files selected for processing (8)
  • apps/workspace-engine/pkg/events/handler/handler.go (2 hunks)
  • apps/workspace-engine/pkg/selector/resources.go (5 hunks)
  • apps/workspace-engine/pkg/server/openapi/deployments/server.go (1 hunks)
  • apps/workspace-engine/pkg/server/openapi/environments/server.go (1 hunks)
  • apps/workspace-engine/pkg/server/openapi/policies/policies.go (1 hunks)
  • apps/workspace-engine/pkg/server/openapi/releasetargets/releasetargets.go (2 hunks)
  • apps/workspace-engine/pkg/server/openapi/utils/utils.go (1 hunks)
  • apps/workspace-engine/pkg/workspace/populate_workspace.go (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
apps/workspace-engine/**/*.go

📄 CodeRabbit inference engine (apps/workspace-engine/CLAUDE.md)

apps/workspace-engine/**/*.go: Do not add extraneous inline comments that state the obvious
Do not add comments that simply restate what the code does
Do not add comments for standard Go patterns (e.g., noting WaitGroup or semaphore usage)
Write comments that explain why, document complex logic/algorithms, provide non-obvious context, include TODO/FIXME, and document exported functions/types/methods

Files:

  • apps/workspace-engine/pkg/server/openapi/environments/server.go
  • apps/workspace-engine/pkg/server/openapi/deployments/server.go
  • apps/workspace-engine/pkg/selector/resources.go
  • apps/workspace-engine/pkg/server/openapi/utils/utils.go
  • apps/workspace-engine/pkg/workspace/populate_workspace.go
  • apps/workspace-engine/pkg/events/handler/handler.go
  • apps/workspace-engine/pkg/server/openapi/policies/policies.go
  • apps/workspace-engine/pkg/server/openapi/releasetargets/releasetargets.go
🧬 Code graph analysis (6)
apps/workspace-engine/pkg/server/openapi/environments/server.go (2)
apps/workspace-engine/pkg/server/openapi/utils/utils.go (1)
  • GetWorkspace (12-33)
apps/workspace-engine/pkg/workspace/workspace.go (1)
  • GetWorkspace (161-168)
apps/workspace-engine/pkg/server/openapi/deployments/server.go (2)
apps/workspace-engine/pkg/server/openapi/utils/utils.go (1)
  • GetWorkspace (12-33)
apps/workspace-engine/pkg/workspace/workspace.go (1)
  • GetWorkspace (161-168)
apps/workspace-engine/pkg/selector/resources.go (2)
apps/workspace-engine/pkg/selector/langs/jsonselector/compare/convert.go (1)
  • ConvertToSelector (12-34)
apps/workspace-engine/pkg/selector/langs/jsonselector/jsonselector.go (1)
  • ConvertToSelector (14-19)
apps/workspace-engine/pkg/server/openapi/utils/utils.go (3)
apps/workspace-engine/pkg/workspace/workspace.go (5)
  • GetWorkspace (161-168)
  • Workspace (41-47)
  • Exists (148-151)
  • New (15-26)
  • Set (153-155)
apps/workspace-engine/pkg/db/workspaces.go (1)
  • GetWorkspaceIDs (9-35)
apps/workspace-engine/pkg/workspace/populate_workspace.go (1)
  • PopulateWorkspaceWithInitialState (8-60)
apps/workspace-engine/pkg/events/handler/handler.go (1)
apps/workspace-engine/pkg/workspace/populate_workspace.go (1)
  • PopulateWorkspaceWithInitialState (8-60)
apps/workspace-engine/pkg/server/openapi/releasetargets/releasetargets.go (1)
apps/workspace-engine/pkg/server/openapi/utils/utils.go (1)
  • GetWorkspace (12-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Typecheck
  • GitHub Check: Lint
  • GitHub Check: build (linux/amd64)

Comment on lines +15 to 21
ws, err := utils.GetWorkspace(c, workspaceId)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to get workspace: " + err.Error(),
})
return
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Apply the workspace not-found error handling pattern here.

As noted in my comment on utils.go lines 18-20, the current error handling returns 500 Internal Server Error for all GetWorkspace failures, including when a workspace doesn't exist in the database. Once utils.GetWorkspace is updated to return a sentinel ErrWorkspaceNotFound error, this handler should detect it and return 404 Not Found instead.

See the suggested fix in apps/workspace-engine/pkg/server/openapi/utils/utils.go at lines 18-20. After implementing the sentinel error pattern there, update this handler to distinguish between 404 and 500 cases.

🤖 Prompt for AI Agents
In apps/workspace-engine/pkg/server/openapi/policies/policies.go around lines 15
to 21, the handler currently treats all errors from utils.GetWorkspace as 500;
update it to check for the sentinel utils.ErrWorkspaceNotFound and return
c.JSON(http.StatusNotFound, ...) when that error is returned, otherwise preserve
the existing InternalServerError behavior; ensure you perform a type/equality
check against utils.ErrWorkspaceNotFound (or errors.Is) and keep the existing
error message content for both branches.

Comment on lines +18 to 24
ws, err := utils.GetWorkspace(c, workspaceId)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Apply the workspace not-found error handling pattern here.

Similar to policies.go, this handler returns 500 Internal Server Error for all GetWorkspace failures. Once the sentinel error pattern is implemented in utils.GetWorkspace (see comment on utils.go lines 18-20), update this handler to return 404 Not Found when the workspace doesn't exist.

Apply the same error-checking pattern suggested for policies.go at lines 15-21.

🤖 Prompt for AI Agents
In apps/workspace-engine/pkg/server/openapi/releasetargets/releasetargets.go
around lines 18 to 24, the handler currently returns 500 for all errors from
utils.GetWorkspace; update it to follow the same sentinel error pattern used in
policies.go (lines 15-21): check if errors.Is(err, utils.ErrWorkspaceNotFound)
and if so respond with c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}),
otherwise respond with 500; ensure you import the errors package if not present
and keep the early-return flow identical to the policies.go example.

Comment on lines +72 to 78
ws, err := utils.GetWorkspace(c, workspaceId)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to get workspace: " + err.Error(),
})
return
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Apply the workspace not-found error handling pattern here.

This is another location where the workspace not-found error should be handled with a 404 Not Found response instead of 500 Internal Server Error.

Apply the same error-checking pattern suggested for policies.go at lines 15-21 and releasetargets.go at lines 18-24.

🤖 Prompt for AI Agents
In apps/workspace-engine/pkg/server/openapi/releasetargets/releasetargets.go
around lines 72 to 78, the handler returns a 500 on any workspace lookup error;
change it to follow the workspace not-found pattern used in policies.go (lines
15-21) and releasetargets.go (lines 18-24) by checking whether the error
indicates the workspace was not found and if so return
c.JSON(http.StatusNotFound, gin.H{"error":"Workspace not found"}), otherwise
return the 500 with the existing error message; keep the error text consistent
with the other handlers.

Comment on lines 18 to 20
if !slices.Contains(dbWorkspaceIds, workspaceId) {
return nil, fmt.Errorf("workspace %s not found in database", workspaceId)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Distinguish "not found" errors for correct HTTP status codes.

Returning a generic error when the workspace doesn't exist in the database causes downstream handlers (policies.go:17, releasetargets.go:20,74) to respond with 500 Internal Server Error instead of 404 Not Found. This breaks REST semantics and may confuse API clients.

Solution: Use a sentinel error type that handlers can detect:

In utils.go:

+import "errors"
+
+var ErrWorkspaceNotFound = errors.New("workspace not found")
+
 func GetWorkspace(c *gin.Context, workspaceId string) (*workspace.Workspace, error) {
 	dbWorkspaceIds, err := db.GetWorkspaceIDs(c.Request.Context())
 	if err != nil {
 		return nil, err
 	}
 
 	if !slices.Contains(dbWorkspaceIds, workspaceId) {
-		return nil, fmt.Errorf("workspace %s not found in database", workspaceId)
+		return nil, fmt.Errorf("%w: %s", ErrWorkspaceNotFound, workspaceId)
 	}

Then in handlers (e.g., policies.go):

 	ws, err := utils.GetWorkspace(c, workspaceId)
 	if err != nil {
+		if errors.Is(err, utils.ErrWorkspaceNotFound) {
+			c.JSON(http.StatusNotFound, gin.H{
+				"error": err.Error(),
+			})
+			return
+		}
 		c.JSON(http.StatusInternalServerError, gin.H{
 			"error": "Failed to get workspace: " + err.Error(),
 		})
 		return
 	}

Apply the same pattern to releasetargets.go at lines 18-24 and 72-78.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/workspace-engine/pkg/server/openapi/utils/utils.go around lines 18-20,
returning a generic fmt.Errorf for a missing workspace causes callers to return
500 instead of 404; define and export a sentinel error variable (e.g.,
ErrWorkspaceNotFound) in that package and return that sentinel when the
workspaceId is not present, then update handlers that call this util
(apps/workspace-engine/pkg/server/openapi/policies.go around line 17 and
apps/workspace-engine/pkg/server/openapi/releasetargets.go around lines 18-24
and 72-78) to detect the sentinel error and translate it to an HTTP 404 response
(only other errors remain 500). Ensure the sentinel is a package-level var
(errors.New(...)) so comparisons use errors.Is or direct equality as
appropriate.

Comment on lines 22 to 32
wsExists := workspace.Exists(workspaceId)
if wsExists {
return workspace.GetWorkspace(workspaceId), nil
}

ws := workspace.New(workspaceId)
if err := workspace.PopulateWorkspaceWithInitialState(c.Request.Context(), ws); err != nil {
return nil, fmt.Errorf("failed to populate workspace with initial state: %w", err)
}
workspace.Set(workspaceId, ws)
return ws, nil
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Potential race condition in workspace initialization.

Between checking workspace.Exists(workspaceId) (line 22) and calling workspace.Set(workspaceId, ws) (line 31), another concurrent request for the same workspace could proceed through the same initialization path. Both requests would:

  1. Create separate workspace instances
  2. Populate from the database (duplicate work)
  3. Call workspace.Set, with the last write winning

While the final state should be consistent (both load the same DB snapshot), this wastes resources and could cause subtle issues if workspace initialization has side effects.

Consider using a sync mechanism to ensure single initialization per workspace:

var (
	workspaceInitLocks sync.Map // map[string]*sync.Mutex
)

func getOrCreateInitLock(workspaceId string) *sync.Mutex {
	lock, _ := workspaceInitLocks.LoadOrStore(workspaceId, &sync.Mutex{})
	return lock.(*sync.Mutex)
}

func GetWorkspace(c *gin.Context, workspaceId string) (*workspace.Workspace, error) {
	dbWorkspaceIds, err := db.GetWorkspaceIDs(c.Request.Context())
	if err != nil {
		return nil, err
	}

	if !slices.Contains(dbWorkspaceIds, workspaceId) {
		return nil, fmt.Errorf("workspace %s not found in database", workspaceId)
	}

	if workspace.Exists(workspaceId) {
		return workspace.GetWorkspace(workspaceId), nil
	}

	lock := getOrCreateInitLock(workspaceId)
	lock.Lock()
	defer lock.Unlock()

	// Double-check after acquiring lock
	if workspace.Exists(workspaceId) {
		return workspace.GetWorkspace(workspaceId), nil
	}

	ws := workspace.New(workspaceId)
	if err := workspace.PopulateWorkspaceWithInitialState(c.Request.Context(), ws); err != nil {
		return nil, fmt.Errorf("failed to populate workspace with initial state: %w", err)
	}
	workspace.Set(workspaceId, ws)
	return ws, nil
}

Alternatively, verify if the current behavior is acceptable by checking:

  1. Whether duplicate initialization causes any data consistency issues
  2. How frequently this code path is hit under concurrent load

🏁 Script executed:

#!/bin/bash
# Search for other callers of PopulateWorkspaceWithInitialState to understand initialization patterns
rg -nP 'PopulateWorkspaceWithInitialState' --type=go -C3

Length of output: 2045


I need to examine the workspace package implementation to verify if the race condition actually exists.


Let me search more broadly:




---

🏁 Script executed:

```shell
#!/bin/bash
# Find and examine workspace package Set and Exists implementations
rg -nP '^\s*func\s+(Set|Exists|New)\s*\(' --type=go -A5 apps/workspace-engine/pkg/workspace/

Length of output: 5483


🏁 Script executed:

#!/bin/bash
# Search for workspace package files and Set/Exists implementation
fd -e go apps/workspace-engine/pkg/workspace/ | head -20

Length of output: 449


🏁 Script executed:

#!/bin/bash
# Find Set, Exists functions in workspace package with more context
rg -nP '(func.*Set|func.*Exists|var.*Map|sync\.)' --type=go apps/workspace-engine/pkg/workspace/ -C2

Length of output: 11177


I need to verify what cmap is and confirm the TOCTOU race condition concern.





---

🏁 Script executed:

```shell
#!/bin/bash
# Find cmap import and definition
rg -nP 'import.*cmap|package cmap' --type=go | head -10

Length of output: 132


🏁 Script executed:

#!/bin/bash
# Look at the utils.go file context to see the full GetWorkspace function
rg -nP 'func GetWorkspace' --type=go -B5 -A15 apps/workspace-engine/pkg/server/openapi/utils/

Length of output: 1768


🏁 Script executed:

#!/bin/bash
# Check if there's any locking in handler.go around workspace initialization
rg -nP 'wsExists|PopulateWorkspaceWithInitialState' --type=go -B10 -A5 apps/workspace-engine/pkg/events/handler/handler.go

Length of output: 936


I need to verify the cmap implementation to confirm the race condition exists.


---

🏁 Script executed:

```shell
#!/bin/bash
# Examine cmap Get and Set implementations
cat apps/workspace-engine/pkg/cmap/concurrent_map.go | head -100

Length of output: 2611


@adityachoudhari26 adityachoudhari26 merged commit c7d2d3a into main Oct 16, 2025
5 of 8 checks passed
@adityachoudhari26 adityachoudhari26 deleted the init-workspace-from-server branch October 16, 2025 21:40
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.

1 participant