-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Better concurrent request handling for model host address (#38)
Like in #36 the reconcile may be affected by external requests. This refactoring helps by reducing lock conflicts * Handle request context timeout * Optimise concurrent access in endpoints type by separating r/w lock and notification lock * Added some tests and Go doc I have also added a benchmark that shows that the new rwlock is ~30% faster than before on my box. But this is all within ns and does not really matter: ``` new: BenchmarkEndpointGroup-12 7667690 154.6 ns/op old: BenchmarkEndpointGroup-12 4968279 234.2 ns/op ``` The key benefit of this PR is handling request timeout
- Loading branch information
Showing
9 changed files
with
291 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
package endpoints | ||
|
||
import ( | ||
"context" | ||
"sync" | ||
"sync/atomic" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
|
||
"k8s.io/apimachinery/pkg/util/rand" | ||
) | ||
|
||
func TestConcurrentAccess(t *testing.T) { | ||
const myService = "myService" | ||
const myPort = "myPort" | ||
|
||
testCases := map[string]struct { | ||
readerCount int | ||
writerCount int | ||
}{ | ||
"lot of reader": {readerCount: 1_000, writerCount: 1}, | ||
"lot of writer": {readerCount: 1, writerCount: 1_000}, | ||
"lot of both": {readerCount: 1_000, writerCount: 1_000}, | ||
} | ||
for name, spec := range testCases { | ||
randomReadFn := []func(g *endpointGroup){ | ||
func(g *endpointGroup) { g.getBestHost(nil, myPort) }, | ||
func(g *endpointGroup) { g.getAllHosts(myPort) }, | ||
func(g *endpointGroup) { g.lenIPs() }, | ||
} | ||
t.Run(name, func(t *testing.T) { | ||
// setup endpoint with one service so that requests are not waiting | ||
endpoint := newEndpointGroup() | ||
endpoint.setIPs( | ||
map[string]struct{}{myService: {}}, | ||
map[string]int32{myPort: 1}, | ||
) | ||
|
||
var startWg, doneWg sync.WaitGroup | ||
startTogether := func(n int, f func()) { | ||
startWg.Add(n) | ||
doneWg.Add(n) | ||
for i := 0; i < n; i++ { | ||
go func() { | ||
startWg.Done() | ||
startWg.Wait() | ||
f() | ||
doneWg.Done() | ||
}() | ||
} | ||
} | ||
// when | ||
startTogether(spec.readerCount, func() { randomReadFn[rand.Intn(len(randomReadFn)-1)](endpoint) }) | ||
startTogether(spec.writerCount, func() { | ||
endpoint.setIPs( | ||
map[string]struct{}{rand.String(1): {}}, | ||
map[string]int32{rand.String(1): 1}, | ||
) | ||
}) | ||
doneWg.Wait() | ||
}) | ||
} | ||
} | ||
|
||
func TestBlockAndWaitForEndpoints(t *testing.T) { | ||
var completed atomic.Int32 | ||
var startWg, doneWg sync.WaitGroup | ||
startTogether := func(n int, f func()) { | ||
startWg.Add(n) | ||
doneWg.Add(n) | ||
for i := 0; i < n; i++ { | ||
go func() { | ||
startWg.Done() | ||
startWg.Wait() | ||
f() | ||
completed.Add(1) | ||
doneWg.Done() | ||
}() | ||
} | ||
} | ||
endpoint := newEndpointGroup() | ||
ctx := context.TODO() | ||
startTogether(100, func() { | ||
endpoint.getBestHost(ctx, rand.String(4)) | ||
}) | ||
startWg.Wait() | ||
|
||
// when broadcast triggered | ||
endpoint.setIPs( | ||
map[string]struct{}{rand.String(4): {}}, | ||
map[string]int32{rand.String(4): 1}, | ||
) | ||
// then | ||
doneWg.Wait() | ||
assert.Equal(t, int32(100), completed.Load()) | ||
} | ||
|
||
func TestAbortOnCtxCancel(t *testing.T) { | ||
ctx, cancel := context.WithCancel(context.Background()) | ||
|
||
var startWg, doneWg sync.WaitGroup | ||
startWg.Add(1) | ||
doneWg.Add(1) | ||
go func(t *testing.T) { | ||
startWg.Wait() | ||
endpoint := newEndpointGroup() | ||
_, err := endpoint.getBestHost(ctx, rand.String(4)) | ||
require.Error(t, err) | ||
doneWg.Done() | ||
}(t) | ||
startWg.Done() | ||
cancel() | ||
|
||
doneWg.Wait() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package endpoints | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
) | ||
|
||
func BenchmarkEndpointGroup(b *testing.B) { | ||
e := newEndpointGroup() | ||
e.setIPs(map[string]struct{}{"10.0.0.1": {}}, map[string]int32{"testPort": 1}) | ||
b.ResetTimer() | ||
b.RunParallel(func(pb *testing.PB) { | ||
for pb.Next() { | ||
_, err := e.getBestHost(context.Background(), "testPort") | ||
if err != nil { | ||
b.Fatal(err) | ||
} | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package endpoints | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestAwaitBestHost(t *testing.T) { | ||
const myService = "myService" | ||
const myPort = "myPort" | ||
|
||
manager := &Manager{endpoints: make(map[string]*endpointGroup, 1)} | ||
manager.getEndpoints(myService). | ||
setIPs(map[string]struct{}{myService: {}}, map[string]int32{myPort: 1}) | ||
|
||
testCases := map[string]struct { | ||
service string | ||
portName string | ||
timeout time.Duration | ||
expErr error | ||
}{ | ||
"all good": { | ||
service: myService, | ||
portName: myPort, | ||
timeout: time.Millisecond, | ||
}, | ||
"unknown port - returns default if only 1": { | ||
service: myService, | ||
portName: "unknownPort", | ||
timeout: time.Millisecond, | ||
}, | ||
"unknown service - blocks until timeout": { | ||
service: "unknownService", | ||
portName: myPort, | ||
timeout: time.Millisecond, | ||
expErr: context.DeadlineExceeded, | ||
}, | ||
// not covered: unknown port with multiple ports on entrypoint | ||
} | ||
|
||
for name, spec := range testCases { | ||
t.Run(name, func(t *testing.T) { | ||
ctx, cancel := context.WithTimeout(context.Background(), spec.timeout) | ||
defer cancel() | ||
|
||
gotHost, gotErr := manager.AwaitHostAddress(ctx, spec.service, spec.portName) | ||
if spec.expErr != nil { | ||
require.ErrorIs(t, spec.expErr, gotErr) | ||
return | ||
} | ||
require.NoError(t, gotErr) | ||
assert.Equal(t, myService+":1", gotHost) | ||
}) | ||
} | ||
} |
Oops, something went wrong.