Skip to content

feat: implement housekeeper election and leadership - #76

Open
lucasoares wants to merge 18 commits into
mainfrom
copilot/fix-21
Open

feat: implement housekeeper election and leadership#76
lucasoares wants to merge 18 commits into
mainfrom
copilot/fix-21

Conversation

@lucasoares

@lucasoares lucasoares commented Jul 6, 2026

Copy link
Copy Markdown
Member

Fixes #21
Partial fix for #30

Copilot AI and others added 9 commits June 5, 2025 17:37
Co-authored-by: lucasoares <10624972+lucasoares@users.noreply.github.com>
Co-authored-by: lucasoares <10624972+lucasoares@users.noreply.github.com>
scheduleTask and unlockMessages were left over from before this PR
introduced scheduleTaskWithDistributedLock and unlockMessagesParallel
and were no longer called anywhere.
ReleaseLock and RefreshLock used a non-atomic Get-then-Del/Expire
sequence to check lock ownership. Between the Get and the mutating
call, the lock could expire and be re-acquired by another instance,
causing this instance to release or refresh a lock it no longer owns.

Add CompareAndDelete/CompareAndExpire to the Cache interface, backed
by atomic Lua scripts in Redis (GET+DEL / GET+PEXPIRE) and a
lock-guarded compare in MemoryCache, and use them in
RedisDistributedLock.ReleaseLock/RefreshLock so ownership is checked
and mutated atomically.
# Conflicts:
#	internal/queue/cache/redis_cache.go
Copilot AI review requested due to automatic review settings July 6, 2026 00:42

Copilot AI left a comment

Copy link
Copy Markdown

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 adds distributed coordination for housekeeper tasks (via Redis-backed locks) and introduces bounded parallelism for unlocking, aiming to support multiple housekeeper instances without conflicting executions.

Changes:

  • Added a DistributedLock abstraction with a Redis implementation using atomic compare-and-delete / compare-and-expire primitives.
  • Updated housekeeper scheduling to use distributed locks (and added metrics “leader” gating).
  • Parallelized unlock processing with configurable concurrency and documented the new distributed mode.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
internal/queue/queue.go Exposes the queue cache to allow constructing distributed locks from the queue instance.
internal/queue/queue_housekeeper.go Changes unlock processing to run in parallel with configurable concurrency.
internal/queue/distributed_lock.go Introduces distributed lock interface and Redis/NoOp implementations.
internal/queue/distributed_lock_test.go Adds unit tests for the distributed lock implementations.
internal/queue/cache/redis_cache.go Adds Redis operations needed for distributed locking (SetNX, CompareAndDelete, CompareAndExpire, etc.).
internal/queue/cache/redis_cache_test.go Adds Redis integration tests for compare-and-delete and compare-and-expire behavior.
internal/queue/cache/redis_cache_scripts.go Adds Lua scripts for atomic compare-and-delete and compare-and-expire.
internal/queue/cache/memory_cache.go Adds in-memory equivalents of the new cache interface methods (best-effort semantics).
internal/queue/cache/cache.go Extends the cache interface with primitives required for distributed locking.
internal/config/housekeeper.go Adds distributed execution config keys and unlock parallelism config.
internal/cmd/deckard/main.go Updates housekeeper scheduling to acquire/release distributed locks and adds metrics leader logic.
docs/distributed-housekeeper.md Documents the distributed housekeeper feature and configuration.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/queue/queue_housekeeper.go Outdated
Comment thread internal/cmd/deckard/main.go Outdated
Comment thread docs/distributed-housekeeper.md Outdated
Comment thread internal/queue/distributed_lock_test.go Outdated
Comment thread internal/queue/cache/cache.go
Comment thread docs/distributed-housekeeper.md Outdated
- Introduced a new `lock` package providing a generic distributed mutual-exclusion primitive.
- Implemented `Locker` interface with a Redis-backed `storeLocker` for distributed locking.
- Added a `noopLocker` for single-instance deployments, preserving original behavior.
- Created tests for `noopLocker` and `storeLocker` to ensure correct functionality.
- Removed legacy `DistributedLock` implementation and its tests.
- Enhanced message structure to include `LockedUntil` timestamp for better lock management.
- Updated queue housekeeper logic to handle message recovery based on `LockedUntil`.
- Added integration tests to verify Redis lock prefix behavior.
- Updated metrics to track housekeeper leadership status.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 29 out of 29 changed files in this pull request and generated 5 comments.

Comment thread internal/cmd/deckard/main.go Outdated
Comment thread internal/config/housekeeper.go Outdated
Comment thread internal/queue/cache/memory_cache.go
Comment thread internal/queue/cache/memory_cache.go
Comment thread internal/metrics/metrics.go

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 8 comments.

Comment thread internal/cmd/deckard/main.go
Comment thread internal/metrics/metrics.go
Comment thread internal/metrics/metrics.go
Comment thread internal/queue/cache/memory_cache.go
Comment thread docs/distributed-housekeeper.md Outdated
Comment thread docs/distributed-housekeeper.md Outdated
Comment thread docs/distributed-housekeeper.md Outdated
Comment thread docs/distributed-housekeeper.md Outdated
- Resolved 13 human review threads
- Fixed thread-requested concurrency and lock semantics issues
- Updated distributed housekeeper docs to match implementation
@lucasoares

Copy link
Copy Markdown
Member Author

PR Review Resolution Summary

PR: feat: implement housekeeper election and leadership (#76) | Branch: copilot/fix-21→main | Last run: 2026-07-05T22:43:00-03:00 | Commit: 8852b88

Threads Processed

# File Source Action Result
1 internal/cmd/deckard/main.go review-thread fixed Added runtime-safe behavior for non-Redis cache mode and warning path
2 internal/config/housekeeper.go review-thread fixed Improved fallback instance-id uniqueness (UnixNano)
3 internal/queue/cache/memory_cache.go review-thread fixed Enforced existence check in compare-and-delete
4 internal/queue/cache/memory_cache.go review-thread fixed Enforced existence+ownership in compare-and-expire
5 internal/metrics/metrics.go review-thread fixed Added lock protection for leader status callback access
6 internal/cmd/deckard/main.go review-thread fixed Added lock renew heartbeat for long-running tasks
7 internal/metrics/metrics.go review-thread fixed Thread-safe callback snapshot before invocation
8 internal/metrics/metrics.go review-thread fixed Race-safe read/write synchronization for callback
9 internal/queue/cache/memory_cache.go review-thread fixed Added TTL tracking + lazy expiration for memory lock keys
10 docs/distributed-housekeeper.md review-thread fixed Corrected lock module description
11 docs/distributed-housekeeper.md review-thread fixed Removed stale static-elector/noop-lock documentation
12 docs/distributed-housekeeper.md review-thread fixed Corrected default instance-id format in env docs
13 docs/distributed-housekeeper.md review-thread fixed Corrected instance identification section

SonarQube

  • Status: skipped by request | Fixed: 0 | Blocked: 0

CI Checks After Fix

Check Before After
Unit tests (go test ./...) not failing in collector run passing

Remaining Open Items

  • None.

Copilot AI review requested due to automatic review settings July 6, 2026 13:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 6 comments.

Comment thread internal/queue/queue_test.go
Comment thread internal/queue/queue_test.go
Comment thread internal/queue/queue_test.go
Comment thread internal/queue/queue_test.go
Comment thread internal/queue/queue_test.go
Comment thread docs/distributed-housekeeper.md Outdated
- Remove parallelism from tests mutating global dtime provider
- Align distributed housekeeper docs with REDIS-only distributed mode
- Restore redis cache implementation after rebase regression
@lucasoares

Copy link
Copy Markdown
Member Author

PR Review Resolution Summary

PR: feat: implement housekeeper election and leadership (#76) | Branch: copilot/fix-21→main | Last run: 2026-07-06T18:16:00-03:00 | Commit: 11983d3

Threads Processed

# File Source Action Result
1 internal/queue/queue_test.go review-thread fixed Removed t.Parallel() from test mutating global dtime provider
2 internal/queue/queue_test.go review-thread fixed Removed t.Parallel() from test mutating global dtime provider
3 internal/queue/queue_test.go review-thread fixed Removed t.Parallel() from test mutating global dtime provider
4 internal/queue/queue_test.go review-thread fixed Removed t.Parallel() from test mutating global dtime provider
5 internal/queue/queue_test.go review-thread fixed Removed t.Parallel() from test mutating global dtime provider
6 docs/distributed-housekeeper.md review-thread fixed Updated docs to match REDIS-only distributed execution behavior

SonarQube

  • Status: skipped by request | Fixed: 0 | Blocked: 0

CI Checks After Fix

Check Before After
Unit tests (go test ./...) not failing in collector run passing

Remaining Open Items

  • None.

@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.04149% with 48 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.23%. Comparing base (4b13b0a) to head (6cacfcb).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
internal/cmd/deckard/main.go 88.71% 14 Missing and 8 partials ⚠️
internal/queue/queue_housekeeper.go 72.34% 9 Missing and 4 partials ⚠️
internal/election/lease_elector.go 83.33% 6 Missing and 3 partials ⚠️
internal/config/housekeeper.go 75.00% 1 Missing and 1 partial ⚠️
internal/queue/queue.go 77.77% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #76      +/-   ##
==========================================
+ Coverage   80.88%   83.23%   +2.34%     
==========================================
  Files          27       30       +3     
  Lines        3071     3465     +394     
==========================================
+ Hits         2484     2884     +400     
+ Misses        418      405      -13     
- Partials      169      176       +7     
Flag Coverage Δ
coverage 83.23% <90.04%> (+2.34%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
Service 80.24% <ø> (ø)
Queue 83.69% <91.17%> (+0.98%) ⬆️
Audit 83.33% <ø> (ø)
Config 91.25% <75.00%> (-1.81%) ⬇️
Command 82.74% <88.71%> (+31.93%) ⬆️
Metrics 91.47% <100.00%> (+2.48%) ⬆️
Cache 83.96% <100.00%> (+2.56%) ⬆️
Storage 79.34% <100.00%> (+0.13%) ⬆️
Helpers 61.59% <ø> (ø)
Files with missing lines Coverage Δ
internal/lock/redis_lock.go 100.00% <100.00%> (ø)
internal/metrics/metrics.go 94.49% <100.00%> (+1.63%) ⬆️
internal/queue/cache/cache.go 0.00% <ø> (ø)
internal/queue/cache/memory_cache.go 92.25% <100.00%> (+1.89%) ⬆️
internal/queue/cache/redis_cache.go 80.36% <100.00%> (+2.27%) ⬆️
internal/queue/message/message.go 100.00% <ø> (ø)
internal/queue/storage/memory_storage.go 77.21% <100.00%> (+0.19%) ⬆️
internal/queue/storage/mongo_storage.go 82.23% <100.00%> (+0.09%) ⬆️
internal/config/housekeeper.go 75.00% <75.00%> (ø)
internal/queue/queue.go 92.15% <77.77%> (-0.53%) ⬇️
... and 3 more

... and 1 file with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 4b13b0a...6cacfcb. Read the comment docs.

Copilot AI review requested due to automatic review settings July 6, 2026 21:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 8 comments.


cache.keys[key] = value
if ttl > 0 {
cache.keyExpiresAt[key] = time.Now().Add(ttl)
}

if ttl > 0 {
cache.keyExpiresAt[key] = time.Now().Add(ttl)
}

if ttl > 0 {
cache.keyExpiresAt[key] = time.Now().Add(ttl)
}

expiresAt, hasExpiration := cache.keyExpiresAt[key]
if hasExpiration && !expiresAt.After(time.Now()) {
Comment on lines +158 to +166
var elector election.Elector
if distributedExecutionEnabled {
elector = election.NewLeaseElector(locker, config.HousekeeperElectionLeaseTTL.GetDuration(), instanceID)
elector.Start(ctx)
metrics.SetLeaderStatusFunc(elector.IsLeader)
} else {
logger.S(ctx).Warnf("Housekeeper distributed execution requires cache type REDIS; running local housekeeper mode with cache type %s", config.CacheType.Get())
metrics.SetLeaderStatusFunc(func() bool { return true })
}
Comment on lines +341 to +343
lockName := fmt.Sprintf("housekeeper:lock:%s", taskName)
lockTTL := config.HousekeeperDistributedExecutionLockTTL.GetDuration()

Comment on lines +414 to 416
lockName := fmt.Sprintf("housekeeper:lock:%s", taskName)
lockTTL := config.HousekeeperDistributedExecutionLockTTL.GetDuration()

Comment on lines +224 to +228
Check lock/election keys in Redis:
```bash
redis-cli KEYS "*housekeeper:lock:*"
redis-cli GET "*housekeeper:election:leader*"
``` No newline at end of file
- internal/config: GetHousekeeperInstanceID (configured/fallback/uniqueness)
- internal/queue/cache: MemoryCache Get/Set/Del/Expire/SetNX/CompareAndDelete/
  CompareAndExpire TTL and lazy-expiration semantics (real object, no mocks)
- internal/lock: TryAcquire expiration and Renew extension through the public
  Locker API backed by a real MemoryCache
- internal/cmd/deckard: startHouseKeeperJobs distributed (Redis leader
  election + atomic lock/renew cycle) and local mode, exercised directly
  with short task delays
Tests using go main() with REDIS cache called shutdown.PerformShutdown
directly, bypassing main()'s <-shutdown.CancelChan branch that releases
housekeeperElector. This leaked the shared Redis leader-election key for
up to the configured lease TTL (15s default), causing later tests in the
same suite/CI run to fail acquiring leadership within a short timeout.

Also flush the cache namespace and extend the leader-election timeout in
the new distributed housekeeper test as defense-in-depth against any
leftover state from other tests sharing the same Redis instance.
Copilot AI review requested due to automatic review settings July 6, 2026 21:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated 1 comment.

Comment on lines +104 to +113
for {
select {
case <-ctx.Done():
return
case <-e.stopCh:
return
case <-ticker.C:
e.campaign(ctx)
}
}
… tests

- internal/queue/cache: RedisCache SetNX/Expire/Close (previously 0%), plus
  error branches for SetNX/Del/Expire/CompareAndDelete/CompareAndExpire via
  a genuinely-canceled context (real go-redis error, not mocked)
- internal/lock: TryAcquire/Release/Renew error branches exercised through a
  real RedisCache-backed Store with a canceled context, reaching 100% package
  coverage (previously required MemoryCache, which never returns errors)
- internal/metrics: SetLeaderStatusFunc and metrifyLeaderStatus exercised
  end-to-end by scraping the real Prometheus registry for the
  deckard_housekeeper_leader gauge value
- internal/election: LeaseElector demotes itself when lease renewal fails
  (external key loss without Stop()), covering campaign()'s lost-leadership
  branch

Whole-repo coverage: 73.0% -> 74.3% (main is 74.2%), 511 tests, 0 failures.
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.

Improve Housekeeper for distributed execution of tasks

3 participants