Skip to content
This repository was archived by the owner on Dec 6, 2025. It is now read-only.

Commit 7d79d6a

Browse files
committed
assigntask
1 parent 0bee7d1 commit 7d79d6a

File tree

6 files changed

+173
-17
lines changed

6 files changed

+173
-17
lines changed

gen/dart/lib/libs/common/v1/conflict.pb.dart

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gen/go/libs/common/v1/conflict.pb.go

Lines changed: 6 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

proto/libs/common/v1/conflict.proto

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ message Conflict {
2121
}
2222

2323
message AttributeConflict {
24-
google.protobuf.Any is = 1; // CAUTION: may be missing, if the is underlying value is missing (e.g., unassigned beds)
25-
google.protobuf.Any want = 2; // CAUTION: may be missing, if the requested value is missing (e.g., unassignment of a bed)
24+
// CAUTION: may be missing, if the is underlying value is missing (e.g., unassigned beds)
25+
// Enums are returned as Int32s
26+
google.protobuf.Any is = 1;
27+
// CAUTION: may be missing, if the requested value is missing (e.g., unassignment of a bed)
28+
// Enums are returned as Int32s
29+
google.protobuf.Any want = 2;
2630
}

services/tasks-svc/internal/task/api/grpc.go

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
pb "gen/services/tasks_svc/v1"
99
"github.com/google/uuid"
1010
zlog "github.com/rs/zerolog"
11+
"github.com/rs/zerolog/log"
1112
"google.golang.org/grpc/codes"
1213
"google.golang.org/grpc/status"
1314
"google.golang.org/protobuf/proto"
@@ -194,9 +195,55 @@ func (s *TaskGrpcService) AssignTask(ctx context.Context, req *pb.AssignTaskRequ
194195
return nil, err
195196
}
196197

197-
consistency, err := s.handlers.Commands.V1.AssignTask(ctx, taskID, userID)
198-
if err != nil {
199-
return nil, err
198+
expConsistency, ok := hwutil.ParseConsistency(req.Consistency)
199+
if !ok {
200+
return nil, common.UnparsableConsistencyError(ctx, "consistency")
201+
}
202+
203+
var consistency uint64
204+
205+
for i := 0; true; i++ {
206+
if i > 10 {
207+
log.Warn().Msg("UpdatePatient: conflict circuit breaker triggered")
208+
return nil, fmt.Errorf("failed conflict resolution")
209+
}
210+
211+
c, conflict, err := s.handlers.Commands.V1.AssignTask(ctx, taskID, userID, expConsistency)
212+
if err != nil {
213+
return nil, err
214+
}
215+
consistency = c
216+
if conflict == nil {
217+
break
218+
}
219+
conflicts := make(map[string]*commonpb.AttributeConflict)
220+
221+
// TODO: find a generic approach
222+
userUpdateRequested := req.UserId != conflict.Is.AssignedUser.UUID.String()
223+
userAlreadyUpdated := conflict.Was.AssignedUser != conflict.Is.AssignedUser
224+
if userUpdateRequested && userAlreadyUpdated {
225+
var is proto.Message = nil
226+
if conflict.Is.AssignedUser.Valid {
227+
is = wrapperspb.String(conflict.Is.AssignedUser.UUID.String())
228+
}
229+
conflicts["user_id"], err = util.AttributeConflict(
230+
is,
231+
wrapperspb.String(req.UserId),
232+
)
233+
if err != nil {
234+
return nil, err
235+
}
236+
}
237+
238+
if len(conflicts) != 0 {
239+
return &pb.AssignTaskResponse{
240+
Conflict: &commonpb.Conflict{ConflictingAttributes: conflicts},
241+
Consistency: strconv.FormatUint(conflict.Consistency, 10),
242+
}, nil
243+
}
244+
245+
// no conflict? retry with new consistency
246+
expConsistency = &conflict.Consistency
200247
}
201248

202249
return &pb.AssignTaskResponse{

services/tasks-svc/internal/task/commands/v1/assign_task.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,38 @@ import (
55
"github.com/google/uuid"
66
"hwes"
77
"tasks-svc/internal/task/aggregate"
8+
"tasks-svc/internal/task/models"
89
)
910

10-
type AssignTaskCommandHandler func(ctx context.Context, taskID, userID uuid.UUID) (uint64, error)
11+
type AssignTaskConflict struct {
12+
Consistency uint64
13+
Was *models.Task
14+
Is *models.Task
15+
}
16+
17+
type AssignTaskCommandHandler func(ctx context.Context, taskID, userID uuid.UUID, expConsistency *uint64) (uint64, *AssignTaskConflict, error)
1118

1219
func NewAssignTaskCommandHandler(as hwes.AggregateStore) AssignTaskCommandHandler {
13-
return func(ctx context.Context, taskID, userID uuid.UUID) (uint64, error) {
14-
task, err := aggregate.LoadTaskAggregate(ctx, as, taskID)
20+
return func(ctx context.Context, taskID, userID uuid.UUID, expConsistency *uint64) (uint64, *AssignTaskConflict, error) {
21+
task, oldState, err := aggregate.LoadTaskAggregateWithSnapshotAt(ctx, as, taskID, expConsistency)
1522
if err != nil {
16-
return 0, err
23+
return 0, nil, err
24+
}
25+
26+
if expConsistency != nil && *expConsistency != task.GetVersion() {
27+
return 0, &AssignTaskConflict{
28+
Consistency: task.GetVersion(),
29+
Was: oldState,
30+
Is: task.Task,
31+
}, nil
1732
}
1833

1934
// TODO: Handle SelfAssignTask when common.GetUserID() is testable
2035
if err := task.AssignTask(ctx, userID); err != nil {
21-
return 0, err
36+
return 0, nil, err
2237
}
2338

24-
return as.Save(ctx, task)
39+
consistency, err := as.Save(ctx, task)
40+
return consistency, nil, err
2541
}
2642
}

services/tasks-svc/stories/TaskConflicts_test.go

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package stories
33
import (
44
"context"
55
pb "gen/services/tasks_svc/v1"
6+
"github.com/google/uuid"
67
"github.com/stretchr/testify/assert"
78
"google.golang.org/protobuf/types/known/timestamppb"
89
"math"
@@ -22,7 +23,6 @@ func TestUpdateTaskConflict_Name(t *testing.T) {
2223
B := "B"
2324
C := "C"
2425

25-
// Only testing HumanReadableIdentifier
2626
testMatrix := []struct {
2727
was string
2828
is string
@@ -92,7 +92,6 @@ func TestUpdateTaskConflict_Description(t *testing.T) {
9292
B := "B"
9393
C := "C"
9494

95-
// Only testing HumanReadableIdentifier
9695
testMatrix := []struct {
9796
was string
9897
is string
@@ -162,7 +161,6 @@ func TestUpdateTaskConflict_DueAt(t *testing.T) {
162161
B := timestamppb.New(time.Now().Add(time.Hour))
163162
C := timestamppb.New(time.Now().Add(time.Hour * 2))
164163

165-
// Only testing HumanReadableIdentifier
166164
testMatrix := []struct {
167165
was *timestamppb.Timestamp
168166
is *timestamppb.Timestamp
@@ -237,7 +235,6 @@ func TestUpdateTaskConflict_Status(t *testing.T) {
237235
B := pb.TaskStatus_TASK_STATUS_IN_PROGRESS
238236
C := pb.TaskStatus_TASK_STATUS_DONE
239237

240-
// Only testing HumanReadableIdentifier
241238
testMatrix := []struct {
242239
was pb.TaskStatus
243240
is *pb.TaskStatus
@@ -300,3 +297,87 @@ func TestUpdateTaskConflict_Status(t *testing.T) {
300297
})
301298
}
302299
}
300+
301+
func TestAssignTaskConflict(t *testing.T) {
302+
ctx := context.Background()
303+
taskClient := taskServiceClient()
304+
305+
patientId := preparePatient(t, ctx, "")
306+
307+
A := uuid.New().String()
308+
B := uuid.New().String()
309+
C := uuid.New().String()
310+
311+
testMatrix := []struct {
312+
was *string
313+
is *string
314+
want string
315+
expectConflict bool
316+
}{
317+
{&A, &B, B, false},
318+
{&A, &B, C, true},
319+
{&A, &A, C, false},
320+
{nil, &A, C, true},
321+
{&A, nil, C, true},
322+
}
323+
324+
for i, o := range testMatrix {
325+
t.Run(t.Name()+"_"+strconv.Itoa(i), func(t *testing.T) {
326+
// WAS
327+
task, err := taskClient.CreateTask(ctx, &pb.CreateTaskRequest{
328+
Name: t.Name(),
329+
Description: nil,
330+
PatientId: patientId,
331+
Public: nil,
332+
DueAt: nil,
333+
InitialStatus: nil,
334+
AssignedUserId: o.was,
335+
Subtasks: nil,
336+
})
337+
assert.NoError(t, err)
338+
339+
id := task.Id
340+
initialConsistency := task.Consistency
341+
342+
// IS
343+
if o.is != nil {
344+
a, err := taskClient.AssignTask(ctx, &pb.AssignTaskRequest{
345+
TaskId: id,
346+
UserId: *o.is,
347+
Consistency: &initialConsistency,
348+
})
349+
assert.NoError(t, err)
350+
assert.Nil(t, a.Conflict)
351+
} else {
352+
a, err := taskClient.UnassignTask(ctx, &pb.UnassignTaskRequest{
353+
TaskId: id,
354+
UserId: *o.was,
355+
Consistency: &initialConsistency,
356+
})
357+
assert.NoError(t, err)
358+
assert.Nil(t, a.Conflict)
359+
}
360+
361+
// WANT
362+
updateRes, err := taskClient.AssignTask(ctx, &pb.AssignTaskRequest{
363+
TaskId: id,
364+
UserId: o.want,
365+
Consistency: &initialConsistency,
366+
})
367+
assert.NoError(t, err)
368+
369+
// EXPECT
370+
assert.Equal(t, o.expectConflict, updateRes.Conflict != nil)
371+
if o.expectConflict {
372+
conflict := updateRes.Conflict.ConflictingAttributes["user_id"]
373+
assert.NotNil(t, conflict)
374+
exp := "want:{[type.googleapis.com/google.protobuf.StringValue]:{value:\"" + o.want + "\"}}"
375+
if o.is != nil {
376+
exp = "is:{[type.googleapis.com/google.protobuf.StringValue]:{value:\"" + *o.is + "\"}} " +
377+
"want:{[type.googleapis.com/google.protobuf.StringValue]:{value:\"" + o.want + "\"}}"
378+
}
379+
assert.Equal(t, exp, strings.Replace(conflict.String(), " ", " ", 1))
380+
}
381+
})
382+
}
383+
}

0 commit comments

Comments
 (0)