-
Notifications
You must be signed in to change notification settings - Fork 817
Allow ingester initialization to continue in background. #2104
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
Changes from all commits
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 |
---|---|---|
|
@@ -293,11 +293,15 @@ func (t *Cortex) initIngester(cfg *Config) (err error) { | |
cfg.Ingester.TSDBConfig = cfg.TSDB | ||
cfg.Ingester.ShardByAllLabels = cfg.Distributor.ShardByAllLabels | ||
|
||
t.ingester, err = ingester.New(cfg.Ingester, cfg.IngesterClient, t.overrides, t.store, prometheus.DefaultRegisterer) | ||
if err != nil { | ||
return | ||
var errfut *ingester.ErrorFuture | ||
t.ingester, errfut = ingester.New(cfg.Ingester, cfg.IngesterClient, t.overrides, t.store, prometheus.DefaultRegisterer) | ||
if done, err := errfut.Get(); done && err != nil { | ||
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. Futures feel needlessly complicated here, and an anti-pattern with Go. Concurrency should be left to the caller, so I believe the module init function should call 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. Hmm, that is a valid point. Back to drawing board. 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. However, by contract the ingester should be populated in the |
||
return err | ||
} | ||
|
||
// if ingester initialization isn't done yet, we go ahead. Services that use ingester must check its | ||
// readiness status and eventually wait for it to finish initialization. | ||
|
||
client.RegisterIngesterServer(t.server.GRPC, t.ingester) | ||
grpc_health_v1.RegisterHealthServer(t.server.GRPC, t.ingester) | ||
t.server.HTTP.Path("/ready").Handler(http.HandlerFunc(t.ingester.ReadinessHandler)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
package ingester | ||
|
||
import ( | ||
"context" | ||
"sync" | ||
) | ||
|
||
// NewErrorFuture creates unfinished future. Must be finished by calling Finish method eventually. | ||
func NewErrorFuture() *ErrorFuture { | ||
return &ErrorFuture{ | ||
ch: make(chan struct{}), | ||
} | ||
} | ||
|
||
// NewFinishedErrorFuture creates finished future with given (possibly nil) error. | ||
func NewFinishedErrorFuture(err error) *ErrorFuture { | ||
ef := NewErrorFuture() | ||
ef.Finish(err) | ||
return ef | ||
} | ||
|
||
// This is a Future object, for holding error. | ||
type ErrorFuture struct { | ||
mu sync.Mutex | ||
done bool | ||
err error | ||
ch chan struct{} // used by waiters | ||
} | ||
|
||
// Returns true if this future is done, and associated error. If future is not done yet, returns false. | ||
func (f *ErrorFuture) Get() (bool, error) { | ||
f.mu.Lock() | ||
defer f.mu.Unlock() | ||
return f.done, f.err | ||
} | ||
|
||
// Waits for future to finish, and returns true and associated error set via Finish method. | ||
// If context is finished first, returns false and error from context instead. | ||
func (f *ErrorFuture) WaitAndGet(ctx context.Context) (bool, error) { | ||
d, err := f.Get() | ||
if d { | ||
return true, err | ||
} | ||
|
||
select { | ||
case <-f.ch: | ||
return f.Get() // must return true now | ||
case <-ctx.Done(): | ||
return false, ctx.Err() | ||
} | ||
} | ||
|
||
// Mark this future as finished. Can only be called once. | ||
func (f *ErrorFuture) Finish(err error) { | ||
f.mu.Lock() | ||
defer f.mu.Unlock() | ||
|
||
f.done = true | ||
f.err = err | ||
close(f.ch) // will panic, if called twice, which is fine. | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package ingester | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/require" | ||
"golang.org/x/tools/go/ssa/interp/testdata/src/errors" | ||
) | ||
|
||
func TestBasicErrorFuture(t *testing.T) { | ||
ef := NewErrorFuture() | ||
|
||
done, err := ef.Get() | ||
require.False(t, done) | ||
require.NoError(t, err) | ||
|
||
ef.Finish(nil) | ||
|
||
done, err = ef.Get() | ||
require.True(t, done) | ||
require.NoError(t, err) | ||
} | ||
|
||
func TestAsyncErrorFutureWithSuccess(t *testing.T) { | ||
ef := NewErrorFuture() | ||
go func() { | ||
time.Sleep(100 * time.Millisecond) | ||
ef.Finish(nil) | ||
}() | ||
|
||
done, err := ef.WaitAndGet(context.Background()) | ||
require.True(t, done) | ||
require.NoError(t, err) | ||
} | ||
|
||
func TestAsyncErrorFutureWithError(t *testing.T) { | ||
initError := errors.New("test error") | ||
ef := NewErrorFuture() | ||
go func() { | ||
time.Sleep(100 * time.Millisecond) | ||
ef.Finish(initError) | ||
}() | ||
|
||
done, err := ef.WaitAndGet(context.Background()) | ||
require.True(t, done) | ||
require.Equal(t, initError, err) | ||
} | ||
|
||
func TestAsyncErrorFutureWithTimeout(t *testing.T) { | ||
ef := NewErrorFuture() | ||
go func() { | ||
time.Sleep(500 * time.Millisecond) | ||
ef.Finish(nil) | ||
}() | ||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) | ||
defer cancel() | ||
|
||
done, err := ef.WaitAndGet(ctx) | ||
require.False(t, done) | ||
require.Equal(t, context.DeadlineExceeded, err) | ||
|
||
// wait for finish | ||
done, err = ef.WaitAndGet(context.Background()) | ||
require.True(t, done) | ||
require.Equal(t, nil, err) | ||
} | ||
|
||
func TestDoubleFinishPanics(t *testing.T) { | ||
var recovered interface{} | ||
|
||
defer func() { | ||
// make sure we have recovered from panic. | ||
if recovered == nil { | ||
t.Fatal("no recovered panic") | ||
} | ||
}() | ||
|
||
defer func() { | ||
recovered = recover() | ||
}() | ||
|
||
ef := NewErrorFuture() | ||
ef.Finish(nil) | ||
ef.Finish(nil) | ||
// if we get here, there was no panic. | ||
t.Fatal("no panic") | ||
} |
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.
A difference I see is that currently Cortex exits if the initialization fails. After this PR it doesn't and if the ingester initialization fails it will continue to run but not work.
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.
That is a valid point, and I can address that.