-
Notifications
You must be signed in to change notification settings - Fork 104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add leadership package #661
base: main
Are you sure you want to change the base?
Changes from 3 commits
96c9f3e
8ed6d5d
0da77e0
4b19017
f39ef3f
935699b
294efc4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
//go:generate gomarkdoc -o README.md --repository.default-branch main | ||
//go:generate gomarkdoc -o README.md --repository.default-branch main --repository.url https://github.com/formancehq/ledger | ||
package ledger |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,86 @@ | ||||||||||||||||||||||||
package leadership | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
import ( | ||||||||||||||||||||||||
"sync" | ||||||||||||||||||||||||
) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
type listener struct { | ||||||||||||||||||||||||
channel chan Leadership | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
type Broadcaster struct { | ||||||||||||||||||||||||
mu *sync.Mutex | ||||||||||||||||||||||||
t *Leadership | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
inner []listener | ||||||||||||||||||||||||
outer chan Leadership | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
func (h *Broadcaster) Actual() Leadership { | ||||||||||||||||||||||||
h.mu.Lock() | ||||||||||||||||||||||||
defer h.mu.Unlock() | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
if h.t == nil { | ||||||||||||||||||||||||
return Leadership{} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
return *h.t | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
func (h *Broadcaster) Subscribe() (<-chan Leadership, func()) { | ||||||||||||||||||||||||
h.mu.Lock() | ||||||||||||||||||||||||
defer h.mu.Unlock() | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
newChannel := make(chan Leadership, 1) | ||||||||||||||||||||||||
index := len(h.inner) | ||||||||||||||||||||||||
h.inner = append(h.inner, listener{ | ||||||||||||||||||||||||
channel: newChannel, | ||||||||||||||||||||||||
}) | ||||||||||||||||||||||||
if h.t != nil { | ||||||||||||||||||||||||
newChannel <- *h.t | ||||||||||||||||||||||||
paul-nicolas marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
return newChannel, func() { | ||||||||||||||||||||||||
h.mu.Lock() | ||||||||||||||||||||||||
defer h.mu.Unlock() | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
if index < len(h.inner)-1 { | ||||||||||||||||||||||||
h.inner = append(h.inner[:index], h.inner[index+1:]...) | ||||||||||||||||||||||||
gfyrag marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||
h.inner = h.inner[:index] | ||||||||||||||||||||||||
gfyrag marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
func (h *Broadcaster) Broadcast(t Leadership) { | ||||||||||||||||||||||||
h.mu.Lock() | ||||||||||||||||||||||||
defer h.mu.Unlock() | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
h.t = &t | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
for _, inner := range h.inner { | ||||||||||||||||||||||||
inner.channel <- t | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
func (h *Broadcaster) Close() { | ||||||||||||||||||||||||
h.mu.Lock() | ||||||||||||||||||||||||
defer h.mu.Unlock() | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
for _, inner := range h.inner { | ||||||||||||||||||||||||
close(inner.channel) | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
func (h *Broadcaster) CountListeners() int { | ||||||||||||||||||||||||
h.mu.Lock() | ||||||||||||||||||||||||
defer h.mu.Unlock() | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
return len(h.inner) | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
func NewSignal() *Broadcaster { | ||||||||||||||||||||||||
return &Broadcaster{ | ||||||||||||||||||||||||
outer: make(chan Leadership), | ||||||||||||||||||||||||
mu: &sync.Mutex{}, | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Improve constructor naming and initialization. The
Apply this diff to improve the constructor: -func NewSignal() *Broadcaster {
+func NewBroadcaster() *Broadcaster {
return &Broadcaster{
- outer: make(chan Leadership),
- mu: &sync.Mutex{},
+ mu: sync.Mutex{},
}
} 📝 Committable suggestion
Suggested change
|
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,43 @@ | ||||||||||||||
package leadership | ||||||||||||||
|
||||||||||||||
import ( | ||||||||||||||
"context" | ||||||||||||||
"sync" | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
type contextKey string | ||||||||||||||
|
||||||||||||||
var holderContextKey contextKey = "holder" | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Use a private type for context key to prevent collisions. Using a string type for context keys is not recommended as it can lead to key collisions. Instead, use a private unexported type. Apply this diff to make the context key more robust: -type contextKey string
+type contextKey struct{}
-var holderContextKey contextKey = "holder"
+var holderContextKey = &contextKey{} 📝 Committable suggestion
Suggested change
|
||||||||||||||
|
||||||||||||||
func ContextWithLeadershipInfo(ctx context.Context) context.Context { | ||||||||||||||
return context.WithValue(ctx, holderContextKey, &holder{}) | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
func IsLeader(ctx context.Context) bool { | ||||||||||||||
h := ctx.Value(holderContextKey) | ||||||||||||||
if h == nil { | ||||||||||||||
return false | ||||||||||||||
} | ||||||||||||||
holder := h.(*holder) | ||||||||||||||
holder.Lock() | ||||||||||||||
defer holder.Unlock() | ||||||||||||||
|
||||||||||||||
return holder.isLeader | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
func setIsLeader(ctx context.Context, isLeader bool) { | ||||||||||||||
h := ctx.Value(holderContextKey) | ||||||||||||||
if h == nil { | ||||||||||||||
return | ||||||||||||||
} | ||||||||||||||
holder := h.(*holder) | ||||||||||||||
holder.Lock() | ||||||||||||||
defer holder.Unlock() | ||||||||||||||
|
||||||||||||||
holder.isLeader = isLeader | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
type holder struct { | ||||||||||||||
sync.Mutex | ||||||||||||||
isLeader bool | ||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package leadership | ||
|
||
type Leadership struct { | ||
Acquired bool | ||
DB DBHandle | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package leadership | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"github.com/uptrace/bun" | ||
) | ||
|
||
const leadershipAdvisoryLockKey = 123456789 | ||
|
||
type DBHandle interface { | ||
bun.IDB | ||
Close() error | ||
} | ||
|
||
// Locker take a lock at process level | ||
// It returns a bun.IDB which MUST be invalidated when the lock is lost | ||
type Locker interface { | ||
Take(ctx context.Context) (DBHandle, error) | ||
} | ||
|
||
type defaultLocker struct { | ||
db *bun.DB | ||
} | ||
|
||
func (p *defaultLocker) Take(ctx context.Context) (DBHandle, error) { | ||
conn, err := p.db.Conn(ctx) | ||
if err != nil { | ||
return nil, fmt.Errorf("error opening new connection: %w", err) | ||
} | ||
|
||
ret := conn.QueryRowContext(ctx, "select pg_try_advisory_lock(?)", leadershipAdvisoryLockKey) | ||
if ret.Err() != nil { | ||
_ = conn.Close() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle connection close errors. The error from - _ = conn.Close()
+ if closeErr := conn.Close(); closeErr != nil {
+ return nil, fmt.Errorf("error closing connection after lock acquisition failure: %w (original error: %v)", closeErr, err)
+ } Also applies to: 40-40, 45-45 |
||
return nil, fmt.Errorf("error acquiring lock: %w", ret.Err()) | ||
} | ||
|
||
var acquired bool | ||
if err := ret.Scan(&acquired); err != nil { | ||
_ = conn.Close() | ||
panic(err) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid using Using Replace the if err := ret.Scan(&acquired); err != nil {
_ = conn.Close()
- panic(err)
+ return false, nil, fmt.Errorf("error scanning result: %w", err)
}
|
||
} | ||
|
||
if !acquired { | ||
_ = conn.Close() | ||
return nil, nil | ||
} | ||
|
||
return conn, nil | ||
} | ||
|
||
func NewDefaultLocker(db *bun.DB) Locker { | ||
return &defaultLocker{db: db} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
//go:build it | ||
|
||
package leadership | ||
|
||
import ( | ||
. "github.com/formancehq/go-libs/v2/testing/utils" | ||
"testing" | ||
|
||
"github.com/formancehq/go-libs/v2/logging" | ||
"github.com/formancehq/go-libs/v2/testing/docker" | ||
"github.com/formancehq/go-libs/v2/testing/platform/pgtesting" | ||
) | ||
|
||
var ( | ||
srv *pgtesting.PostgresServer | ||
) | ||
|
||
func TestMain(m *testing.M) { | ||
WithTestMain(func(t *TestingTForMain) int { | ||
srv = pgtesting.CreatePostgresServer(t, docker.NewPool(t, logging.Testing()), pgtesting.WithExtension("pgcrypto")) | ||
|
||
return m.Run() | ||
}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
package leadership | ||
|
||
import ( | ||
"context" | ||
"github.com/formancehq/go-libs/v2/logging" | ||
"time" | ||
) | ||
|
||
type Manager struct { | ||
locker Locker | ||
changes *Broadcaster | ||
logger logging.Logger | ||
retryPeriod time.Duration | ||
stopChannel chan chan struct{} | ||
} | ||
|
||
func (m *Manager) Run(ctx context.Context) { | ||
var ( | ||
db DBHandle | ||
nextRetry = time.After(time.Duration(0)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It spams ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sry just at init There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, just the first time |
||
err error | ||
) | ||
for { | ||
select { | ||
case ch := <-m.stopChannel: | ||
if db != nil { | ||
m.logger.Info("leadership lost") | ||
_ = db.Close() | ||
setIsLeader(ctx, false) | ||
m.changes.Broadcast(Leadership{}) | ||
} | ||
close(ch) | ||
close(m.stopChannel) | ||
return | ||
case <-nextRetry: | ||
db, err = m.locker.Take(ctx) | ||
if err != nil || db == nil { | ||
if err != nil { | ||
m.logger.Error("error acquiring lock", err) | ||
} | ||
nextRetry = time.After(m.retryPeriod) | ||
continue | ||
} | ||
|
||
m.changes.Broadcast(Leadership{ | ||
DB: db, | ||
Acquired: true, | ||
}) | ||
m.logger.Info("leadership acquired") | ||
|
||
setIsLeader(ctx, true) | ||
} | ||
} | ||
} | ||
|
||
func (m *Manager) Stop(ctx context.Context) error { | ||
select { | ||
// if already closed | ||
case <-m.stopChannel: | ||
return nil | ||
default: | ||
ch := make(chan struct{}) | ||
m.stopChannel <- ch | ||
select { | ||
case <-ctx.Done(): | ||
return ctx.Err() | ||
case <-ch: | ||
return nil | ||
} | ||
} | ||
} | ||
|
||
func (m *Manager) GetSignal() *Broadcaster { | ||
return m.changes | ||
} | ||
|
||
func NewManager(locker Locker, logger logging.Logger, options ...Option) *Manager { | ||
l := &Manager{ | ||
locker: locker, | ||
logger: logger, | ||
changes: NewSignal(), | ||
retryPeriod: 2 * time.Second, | ||
stopChannel: make(chan chan struct{}), | ||
} | ||
|
||
for _, option := range options { | ||
option(l) | ||
} | ||
|
||
return l | ||
} | ||
|
||
type Option func(leadership *Manager) | ||
|
||
func WithRetryPeriod(duration time.Duration) Option { | ||
return func(leadership *Manager) { | ||
leadership.retryPeriod = duration | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Use mutex by value and remove unused channel.
The
Broadcaster
struct has two issues:outer
channel is declared but never used in the implementation.Apply this diff to fix these issues:
📝 Committable suggestion