Skip to content

Commit 6718ab2

Browse files
authored
Merge pull request #3 from EmbarkStudios/do/batch-ticket-deletion
feat: batch ticket deletion api for open-match
2 parents 01d25d5 + 1fdd283 commit 6718ab2

16 files changed

+637
-40
lines changed

api/frontend.proto

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,18 @@ message GetBackfillRequest {
123123
}
124124

125125
// UpdateBackfillRequest - update searchFields, extensions and set assignment.
126-
//
126+
//
127127
// BETA FEATURE WARNING: This Request message is not finalized and still subject
128128
// to possible change or removal.
129129
message UpdateBackfillRequest {
130130
// A Backfill object with ID set and fields to update.
131131
Backfill backfill = 1;
132132
}
133133

134+
message DeleteTicketsRequest {
135+
// TicketIds of generated Tickets to be deleted.
136+
repeated string ticket_ids = 1;
137+
}
134138

135139
// The FrontendService implements APIs to manage and query status of a Tickets.
136140
service FrontendService {
@@ -146,7 +150,7 @@ service FrontendService {
146150
}
147151

148152
// DeleteTicket immediately stops Open Match from using the Ticket for matchmaking and removes the Ticket from state storage.
149-
// The client should delete the Ticket when finished matchmaking with it.
153+
// The client should delete the Ticket when finished matchmaking with it.
150154
rpc DeleteTicket(DeleteTicketRequest) returns (google.protobuf.Empty) {
151155
option (google.api.http) = {
152156
delete: "/v1/frontendservice/tickets/{ticket_id}"
@@ -161,7 +165,7 @@ service FrontendService {
161165
}
162166

163167
// WatchAssignments stream back Assignment of the specified TicketId if it is updated.
164-
// - If the Assignment is not updated, GetAssignment will retry using the configured backoff strategy.
168+
// - If the Assignment is not updated, GetAssignment will retry using the configured backoff strategy.
165169
rpc WatchAssignments(WatchAssignmentsRequest)
166170
returns (stream WatchAssignmentsResponse) {
167171
option (google.api.http) = {
@@ -208,7 +212,7 @@ service FrontendService {
208212
get: "/v1/frontendservice/backfills/{backfill_id}"
209213
};
210214
}
211-
215+
212216
// UpdateBackfill updates search_fields and extensions for the backfill with the provided id.
213217
// Any tickets waiting for this backfill will be returned to the active pool, no longer pending.
214218
// BETA FEATURE WARNING: This call and the associated Request and Response
@@ -228,4 +232,12 @@ service FrontendService {
228232
get: "/v1/frontendservice/backfills/{backfill_id}/tickets"
229233
};
230234
}
235+
236+
// DeleteTickets immediately stops Open Match from using the Tickets for matchmaking and removes the Tickets from state storage.
237+
// The client should delete the Tickets when finished matchmaking with them.
238+
rpc DeleteTickets(DeleteTicketsRequest) returns (google.protobuf.Empty) {
239+
option (google.api.http) = {
240+
delete: "/v1/frontendservice/tickets"
241+
};
242+
}
231243
}

api/frontend.swagger.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,48 @@
273273
}
274274
},
275275
"/v1/frontendservice/tickets": {
276+
"delete": {
277+
"summary": "DeleteTickets immediately stops Open Match from using the Tickets for matchmaking and removes the Tickets from state storage.\nThe client should delete the Tickets when finished matchmaking with them.",
278+
"operationId": "FrontendService_DeleteTickets",
279+
"responses": {
280+
"200": {
281+
"description": "A successful response.",
282+
"schema": {
283+
"type": "object",
284+
"properties": {}
285+
}
286+
},
287+
"404": {
288+
"description": "Returned when the resource does not exist.",
289+
"schema": {
290+
"type": "string",
291+
"format": "string"
292+
}
293+
},
294+
"default": {
295+
"description": "An unexpected error response.",
296+
"schema": {
297+
"$ref": "#/definitions/rpcStatus"
298+
}
299+
}
300+
},
301+
"parameters": [
302+
{
303+
"name": "ticket_ids",
304+
"description": "TicketIds of generated Tickets to be deleted.",
305+
"in": "query",
306+
"required": false,
307+
"type": "array",
308+
"items": {
309+
"type": "string"
310+
},
311+
"collectionFormat": "multi"
312+
}
313+
],
314+
"tags": [
315+
"FrontendService"
316+
]
317+
},
276318
"post": {
277319
"summary": "CreateTicket assigns an unique TicketId to the input Ticket and record it in state storage.\nA ticket is considered as ready for matchmaking once it is created.\n - If a TicketId exists in a Ticket request, an auto-generated TicketId will override this field.\n - If SearchFields exist in a Ticket, CreateTicket will also index these fields such that one can query the ticket with query.QueryTickets function.",
278320
"operationId": "FrontendService_CreateTicket",

internal/app/frontend/frontend_service.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,3 +427,48 @@ func (s *frontendService) GetBackfillTickets(ctx context.Context, req *pb.GetBac
427427

428428
return resp, nil
429429
}
430+
431+
// DeleteTickets immediately stops Open Match from using the Tickets for matchmaking and removes the Tickets from state storage.
432+
// The client must delete the Tickets when finished matchmaking with it.
433+
// - If SearchFields exist in a Ticket, DeleteTickets will deindex the fields lazily.
434+
//
435+
// Users may still be able to assign/get tickets after calling DeleteTickets on it.
436+
func (s *frontendService) DeleteTickets(ctx context.Context, req *pb.DeleteTicketsRequest) (*emptypb.Empty, error) {
437+
err := doDeleteTickets(ctx, req.GetTicketIds(), s.store)
438+
if err != nil {
439+
return nil, err
440+
}
441+
return &emptypb.Empty{}, nil
442+
}
443+
444+
func doDeleteTickets(ctx context.Context, ids []string, store statestore.Service) error {
445+
// Deindex these Tickets to remove it from matchmaking pool.
446+
err := store.DeindexTickets(ctx, ids)
447+
if err != nil {
448+
return err
449+
}
450+
451+
//'lazy' tickets delete that should be called after one or more tickets
452+
// have been deindexed.
453+
go func() {
454+
ctx, span := trace.StartSpan(context.Background(), "open-match/frontend.DeleteTicketsLazy")
455+
defer span.End()
456+
err := store.DeleteTickets(ctx, ids)
457+
if err != nil {
458+
logger.WithFields(logrus.Fields{
459+
"error": err.Error(),
460+
"ids": ids,
461+
}).Error("failed to delete tickets")
462+
}
463+
err = store.DeleteTicketsFromPendingRelease(ctx, ids)
464+
if err != nil {
465+
logger.WithFields(logrus.Fields{
466+
"error": err.Error(),
467+
"ids": ids,
468+
}).Error("failed to delete tickets from pendingRelease")
469+
}
470+
// TODO: If other redis queues are implemented or we have custom index fields
471+
// created by Open Match, those need to be cleaned up here.
472+
}()
473+
return nil
474+
}

internal/app/frontend/frontend_service_test.go

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -461,8 +461,8 @@ func TestDoDeleteTicket(t *testing.T) {
461461
{
462462
description: "expect ok code",
463463
preAction: func(ctx context.Context, _ context.CancelFunc, store statestore.Service) {
464-
store.CreateTicket(ctx, fakeTicket)
465-
store.IndexTicket(ctx, fakeTicket)
464+
_ = store.CreateTicket(ctx, fakeTicket)
465+
_ = store.IndexTicket(ctx, fakeTicket)
466466
},
467467
},
468468
}
@@ -482,6 +482,81 @@ func TestDoDeleteTicket(t *testing.T) {
482482
}
483483
}
484484

485+
func TestDoDeleteTickets(t *testing.T) {
486+
fakeTickets := []*pb.Ticket{
487+
{
488+
Id: "1",
489+
SearchFields: &pb.SearchFields{
490+
DoubleArgs: map[string]float64{
491+
"test-arg": 1,
492+
},
493+
},
494+
},
495+
{
496+
Id: "2",
497+
SearchFields: &pb.SearchFields{
498+
DoubleArgs: map[string]float64{
499+
"test-arg": 2,
500+
},
501+
},
502+
},
503+
{
504+
Id: "3",
505+
SearchFields: &pb.SearchFields{
506+
DoubleArgs: map[string]float64{
507+
"test-arg": 3,
508+
},
509+
},
510+
},
511+
}
512+
513+
tests := []struct {
514+
description string
515+
preAction func(context.Context, context.CancelFunc, statestore.Service)
516+
wantCode codes.Code
517+
}{
518+
{
519+
description: "expect unavailable code since context is canceled before being called",
520+
preAction: func(_ context.Context, cancel context.CancelFunc, _ statestore.Service) {
521+
cancel()
522+
},
523+
wantCode: codes.Unavailable,
524+
},
525+
{
526+
description: "expect ok code since delete tickets does not care about if ticket exists or not",
527+
preAction: func(_ context.Context, _ context.CancelFunc, _ statestore.Service) {},
528+
wantCode: codes.OK,
529+
},
530+
{
531+
description: "expect ok code",
532+
preAction: func(ctx context.Context, _ context.CancelFunc, store statestore.Service) {
533+
for _, ticket := range fakeTickets {
534+
_ = store.CreateTicket(ctx, ticket)
535+
_ = store.IndexTicket(ctx, ticket)
536+
}
537+
},
538+
},
539+
}
540+
541+
for _, test := range tests {
542+
test := test
543+
t.Run(test.description, func(t *testing.T) {
544+
ctx, cancel := context.WithCancel(utilTesting.NewContext(t))
545+
store, closer := statestoreTesting.NewStoreServiceForTesting(t, viper.New())
546+
defer closer()
547+
548+
test.preAction(ctx, cancel, store)
549+
ids := make([]string, len(fakeTickets))
550+
for i := 0; i < len(fakeTickets); i++ {
551+
ids[i] = fakeTickets[i].Id
552+
}
553+
554+
err := doDeleteTickets(ctx, ids, store)
555+
require.Equal(t, test.wantCode.String(), status.Convert(err).Code().String())
556+
})
557+
}
558+
}
559+
485560
func TestDoGetTicket(t *testing.T) {
486561
fakeTicket := &pb.Ticket{
487562
Id: "1",

internal/statestore/instrumented.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ func (is *instrumentedService) DeleteTicket(ctx context.Context, id string) erro
5353
return is.s.DeleteTicket(ctx, id)
5454
}
5555

56+
func (is *instrumentedService) DeleteTickets(ctx context.Context, ids []string) error {
57+
ctx, span := trace.StartSpan(ctx, "statestore/instrumented.DeleteTickets")
58+
defer span.End()
59+
return is.s.DeleteTickets(ctx, ids)
60+
}
61+
5662
func (is *instrumentedService) IndexTicket(ctx context.Context, ticket *pb.Ticket) error {
5763
ctx, span := trace.StartSpan(ctx, "statestore/instrumented.IndexTicket")
5864
defer span.End()
@@ -65,6 +71,12 @@ func (is *instrumentedService) DeindexTicket(ctx context.Context, id string) err
6571
return is.s.DeindexTicket(ctx, id)
6672
}
6773

74+
func (is *instrumentedService) DeindexTickets(ctx context.Context, ids []string) error {
75+
ctx, span := trace.StartSpan(ctx, "statestore/instrumented.DeindexTickets")
76+
defer span.End()
77+
return is.s.DeindexTickets(ctx, ids)
78+
}
79+
6880
func (is *instrumentedService) GetTickets(ctx context.Context, ids []string) ([]*pb.Ticket, error) {
6981
ctx, span := trace.StartSpan(ctx, "statestore/instrumented.GetTickets")
7082
defer span.End()

internal/statestore/public.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,19 @@ type Service interface {
4343
// This method succeeds if the Ticket does not exist.
4444
DeleteTicket(ctx context.Context, id string) error
4545

46+
// DeleteTickets removes the Tickets with the specified id from state storage.
47+
// This method succeeds if any of the Tickets do not exist.
48+
DeleteTickets(ctx context.Context, ids []string) error
49+
4650
// IndexTicket adds the ticket to the index.
4751
IndexTicket(ctx context.Context, ticket *pb.Ticket) error
4852

4953
// DeindexTicket removes specified ticket from the index. The Ticket continues to exist.
5054
DeindexTicket(ctx context.Context, id string) error
5155

56+
// DeindexTickets removes specified tickets from the index. The Tickets continue to exist.
57+
DeindexTickets(ctx context.Context, ids []string) error
58+
5259
// GetIndexedIDSet returns the ids of all tickets currently indexed.
5360
GetIndexedIDSet(ctx context.Context) (map[string]struct{}, error)
5461

internal/statestore/ticket.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,32 @@ func (rb *redisBackend) DeleteTicket(ctx context.Context, id string) error {
118118
return nil
119119
}
120120

121+
// DeleteTickets removes the Tickets with the specified id from state storage.
122+
func (rb *redisBackend) DeleteTickets(ctx context.Context, ids []string) error {
123+
redisConn, err := rb.redisPool.GetContext(ctx)
124+
if err != nil {
125+
return status.Errorf(codes.Unavailable, "DeleteTickets, id: %v, failed to connect to redis: %v", ids, err)
126+
}
127+
defer handleConnectionClose(&redisConn)
128+
129+
args := make([]any, len(ids))
130+
for i, id := range ids {
131+
args[i] = id
132+
}
133+
134+
value, err := redis.Int(redisConn.Do("DEL", args...))
135+
if err != nil {
136+
err = errors.Wrapf(err, "failed to delete tickets from state storage, ids: %v", ids)
137+
return status.Errorf(codes.Internal, "%v", err)
138+
}
139+
140+
if value == 0 {
141+
return status.Errorf(codes.NotFound, "Ticket ids: %s not found", ids)
142+
}
143+
144+
return nil
145+
}
146+
121147
// IndexTicket indexes the Ticket id for the configured index fields.
122148
func (rb *redisBackend) IndexTicket(ctx context.Context, ticket *pb.Ticket) error {
123149
redisConn, err := rb.redisPool.GetContext(ctx)
@@ -152,6 +178,29 @@ func (rb *redisBackend) DeindexTicket(ctx context.Context, id string) error {
152178
return nil
153179
}
154180

181+
// DeindexTickets removes the indexing for the specified Tickets. Only the indexes are removed but Tickets continues to exist.
182+
func (rb *redisBackend) DeindexTickets(ctx context.Context, ids []string) error {
183+
redisConn, err := rb.redisPool.GetContext(ctx)
184+
if err != nil {
185+
return status.Errorf(codes.Unavailable, "DeindexTickets, id: %v, failed to connect to redis: %v", ids, err)
186+
}
187+
defer handleConnectionClose(&redisConn)
188+
189+
args := make([]any, len(ids)+1)
190+
args[0] = allTickets
191+
for i, id := range ids {
192+
args[i+1] = id
193+
}
194+
195+
err = redisConn.Send("SREM", args...)
196+
if err != nil {
197+
err = errors.Wrapf(err, "failed to remove ticket from all tickets, id: %v", ids)
198+
return status.Errorf(codes.Internal, "%v", err)
199+
}
200+
201+
return nil
202+
}
203+
155204
// GetIndexedIds returns the ids of all tickets currently indexed.
156205
func (rb *redisBackend) GetIndexedIDSet(ctx context.Context) (map[string]struct{}, error) {
157206
redisConn, err := rb.redisPool.GetContext(ctx)

0 commit comments

Comments
 (0)