Skip to content

Include entity data in delete watch events#600

Merged
evanphx merged 1 commit intomainfrom
evan/better-delete
Feb 13, 2026
Merged

Include entity data in delete watch events#600
evanphx merged 1 commit intomainfrom
evan/better-delete

Conversation

@evanphx
Copy link
Copy Markdown
Contributor

@evanphx evanphx commented Feb 12, 2026

Summary

  • When a watched entity is deleted, watchers now receive the full entity data from before deletion, eliminating the need for watchers to cache entities on their side
  • WatchEntity uses etcd's WithPrevKV() to get the previous entity value directly from the watch event — no extra round-trip
  • WatchIndex fetches the entity via GetEntity (entity still exists since index entries are deleted first), with a fallback to the new GetEntityAtRevision method for robustness
  • Adds GetEntityAtRevision to the Store interface for revision-based reads from etcd

Test plan

  • Store-level TestWatchEntity_DeleteIncludesEntity against real etcd
  • Server-level TestEntityServer_WatchEntity_DeleteIncludesEntity against mock
  • Server-level TestEntityServer_WatchIndex_DeleteIncludesEntity against mock
  • Server-level TestEntityServer_WatchEntity_DeleteIncludesEntity_Etcd against real etcd
  • Server-level TestEntityServer_WatchIndex_DeleteIncludesEntity_Etcd against real etcd
  • Full pkg/entity test suite (229 tests passing)
  • Full servers/entityserver test suite (23 tests passing)

@evanphx evanphx requested a review from a team as a code owner February 12, 2026 22:41
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 12, 2026

📝 Walkthrough

Walkthrough

Adds revision-aware reads and preserves pre-deletion state for watches. MockStore gains a deletedEntities map, GetEntityAtRevision and WaitForEntityWatcher. The Store interface and EtcdStore implement GetEntityAtRevision. Etcd watch requests PrevKV and delete events emit the previous entity when available. Server watch logic fetches the full entity for delete events (attempting GetEntity, then GetEntityAtRevision). Controller filtering no longer skips delete events with recent revisions. Tests added to assert delete events include the full entity payload.


No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
pkg/entity/mock.go (1)

59-69: Mock simplification is acceptable but worth documenting.

GetEntityAtRevision ignores the rev parameter and simply returns the entity from either current or deleted maps. This is a reasonable mock simplification since:

  1. MockStore doesn't track actual etcd revisions
  2. Tests primarily care about whether the entity can be retrieved, not revision semantics

Consider adding a brief comment noting this simplification for future maintainers.

📝 Suggested documentation
 func (m *MockStore) GetEntityAtRevision(ctx context.Context, id Id, rev int64) (*Entity, error) {
+	// Note: Mock ignores rev parameter - returns entity if it exists in current or deleted maps
 	m.mu.RLock()
 	defer m.mu.RUnlock()
servers/entityserver/entityserver_test.go (1)

469-471: Consider using a more robust synchronization mechanism for etcd watch establishment.

The time.Sleep(100 * time.Millisecond) is a common pattern but can be flaky under load. While etcd watches are typically established quickly, this could cause intermittent test failures in CI environments.

Consider implementing a similar WaitFor* helper for EtcdStore or using a slightly longer sleep with a comment explaining why.

📝 Alternative: Add comment explaining the timing
 	// Give the watch time to establish
-	time.Sleep(100 * time.Millisecond)
+	// Note: etcd watch establishment is typically fast (<10ms), but we use 100ms
+	// to account for CI environment variability. Unlike MockStore, EtcdStore
+	// doesn't expose internal watcher tracking for synchronization.
+	time.Sleep(100 * time.Millisecond)
servers/entityserver/entityserver.go (1)

486-509: Robust fallback strategy for fetching entity on delete events.

The two-stage fetch approach is well-designed:

  1. Try GetEntity first since index entries are deleted before the entity key
  2. Fall back to GetEntityAtRevision if the entity was already deleted

One minor observation: the error from the first GetEntity call is silently discarded. While this is intentional (it's expected to fail sometimes), consider logging at debug level to aid troubleshooting.

📝 Optional: Add debug logging for first fetch failure
 				// Try to fetch the entity data for the delete event.
 				// Index entries are deleted before the entity key, so the entity
 				// may still exist. Fall back to a revision-based read if not.
 				en, err := e.Store.GetEntity(ctx, entityId)
 				if err != nil {
+					e.Log.Debug("entity not found at current revision, trying historical read", "id", entityId, "error", err)
 					en, err = e.Store.GetEntityAtRevision(ctx, entityId, event.Kv.ModRevision)
 				}

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

Copy link
Copy Markdown

@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: 1

When a watched entity is deleted, watchers now receive the full entity
data from before deletion instead of just the operation type. This
eliminates the need for watchers to cache entities on their side.

- WatchEntity: uses WithPrevKV() to unmarshal the previous entity value
- WatchIndex: fetches the entity via GetEntity (still exists since index
  entries are deleted first), falling back to GetEntityAtRevision
- Adds GetEntityAtRevision to Store interface for revision-based reads
@evanphx evanphx merged commit 3fd6404 into main Feb 13, 2026
9 checks passed
@evanphx evanphx deleted the evan/better-delete branch February 13, 2026 21:11
phinze added a commit that referenced this pull request Mar 4, 2026
Update GenericController.Delete to receive the decoded entity from
delete watch events, completing the plumbing started in PR #600 which
made entity data available in delete events but stopped short of
threading it through the controller interface.

This allows the sandbox controller to clean up iptables DNAT rules even
when the entity has already been deleted from the store, since the
entity data arrives directly from the watch event rather than requiring
a store lookup.
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.

2 participants