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

Commit ecd16aa

Browse files
committed
assign beds
1 parent e038085 commit ecd16aa

File tree

5 files changed

+209
-22
lines changed

5 files changed

+209
-22
lines changed

proto/libs/common/v1/conflict.proto

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@ message Conflict {
2121
}
2222

2323
message AttributeConflict {
24-
google.protobuf.Any is = 1;
24+
google.protobuf.Any is = 1; // CAUTION: may be missing, if the is underlying value is missing (e.g., unassigned beds)
2525
google.protobuf.Any want = 2;
2626
}

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

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
zlog "github.com/rs/zerolog/log"
1111
"google.golang.org/grpc/codes"
1212
"google.golang.org/grpc/status"
13+
"google.golang.org/protobuf/proto"
1314
"google.golang.org/protobuf/types/known/wrapperspb"
1415
"hwdb"
1516
"hwdb/locale"
@@ -394,11 +395,23 @@ func (s *PatientGrpcService) UpdatePatient(ctx context.Context, req *pb.UpdatePa
394395
return nil, common.UnparsableConsistencyError(ctx, "consistency")
395396
}
396397

397-
consistency, conflict, err := s.handlers.Commands.V1.UpdatePatient(ctx, patientID, expConsistency, req.HumanReadableIdentifier, req.Notes)
398-
if err != nil {
399-
return nil, err
400-
}
401-
if conflict != nil {
398+
var consistency uint64
399+
400+
for i := 0; true; i++ {
401+
if i > 10 {
402+
zlog.Ctx(ctx).Warn().Msg("UpdatePatient: conflict circuit breaker triggered")
403+
return nil, fmt.Errorf("failed conflict resolution")
404+
}
405+
406+
c, conflict, err := s.handlers.Commands.V1.UpdatePatient(ctx, patientID, expConsistency, req.HumanReadableIdentifier, req.Notes)
407+
consistency = c
408+
409+
if err != nil {
410+
return nil, err
411+
}
412+
if conflict == nil {
413+
break
414+
}
402415
conflicts := make(map[string]*commonpb.AttributeConflict)
403416

404417
// TODO: find a generic approach
@@ -432,6 +445,9 @@ func (s *PatientGrpcService) UpdatePatient(ctx context.Context, req *pb.UpdatePa
432445
Consistency: strconv.FormatUint(conflict.Consistency, 10),
433446
}, nil
434447
}
448+
449+
// no conflict? retry with new consistency
450+
expConsistency = &conflict.Consistency
435451
}
436452

437453
tracking.AddPatientToRecentActivity(ctx, patientID.String())
@@ -457,17 +473,64 @@ func (s *PatientGrpcService) AssignBed(ctx context.Context, req *pb.AssignBedReq
457473
return nil, err
458474
}
459475

460-
consistency, err := s.handlers.Commands.V1.AssignBed(ctx, patientID, bedID)
461-
if err != nil {
462-
return nil, err
476+
expConsistency, ok := hwutil.ParseConsistency(req.Consistency)
477+
if !ok {
478+
return nil, common.UnparsableConsistencyError(ctx, "consistency")
479+
}
480+
481+
var consistency uint64
482+
483+
for i := 0; true; i++ {
484+
if i > 10 {
485+
log.Warn().Msg("AssignBed: conflict circuit breaker triggered")
486+
return nil, fmt.Errorf("failed conflict resolution")
487+
}
488+
489+
c, conflict, err := s.handlers.Commands.V1.AssignBed(ctx, patientID, bedID, expConsistency)
490+
consistency = c
491+
492+
if err != nil {
493+
return nil, err
494+
}
495+
if conflict == nil {
496+
break
497+
}
498+
conflicts := make(map[string]*commonpb.AttributeConflict)
499+
500+
// TODO: find a generic approach
501+
bedUpdateRequested := req.BedId != conflict.Is.BedID.UUID.String()
502+
bedAlreadyUpdated := conflict.Was.BedID != conflict.Is.BedID
503+
if bedUpdateRequested && bedAlreadyUpdated {
504+
var is proto.Message = nil
505+
if conflict.Is.BedID.Valid {
506+
is = wrapperspb.String(conflict.Is.BedID.UUID.String())
507+
}
508+
conflicts["bed_id"], err = util.AttributeConflict(
509+
is,
510+
wrapperspb.String(req.BedId),
511+
)
512+
if err != nil {
513+
return nil, err
514+
}
515+
}
516+
517+
if len(conflicts) != 0 {
518+
return &pb.AssignBedResponse{
519+
Conflict: &commonpb.Conflict{ConflictingAttributes: conflicts},
520+
Consistency: strconv.FormatUint(conflict.Consistency, 10),
521+
}, nil
522+
}
523+
524+
// no conflict? retry with new consistency
525+
expConsistency = &conflict.Consistency
463526
}
464527

465528
log.Info().Str("patientID", patientID.String()).Str("bedID", bedID.String()).Msg("assigned bed to patient")
466529

467530
tracking.AddWardToRecentActivity(ctx, patientID.String())
468531

469532
return &pb.AssignBedResponse{
470-
Conflict: nil, // TODO
533+
Conflict: nil,
471534
Consistency: strconv.FormatUint(consistency, 10),
472535
}, nil
473536
}

services/tasks-svc/internal/patient/commands/v1/assign_bed.go

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

10-
type AssignBedCommandHandler func(ctx context.Context, patientID uuid.UUID, bedID uuid.UUID) (uint64, error)
11+
type AssignBedConflict struct {
12+
Consistency uint64
13+
Was *models.Patient
14+
Is *models.Patient
15+
}
16+
17+
type AssignBedCommandHandler func(ctx context.Context, patientID uuid.UUID, bedID uuid.UUID, expConsistency *uint64) (uint64, *AssignBedConflict, error)
1118

1219
func NewAssignBedCommandHandler(as hwes.AggregateStore) AssignBedCommandHandler {
13-
return func(ctx context.Context, patientID uuid.UUID, bedID uuid.UUID) (uint64, error) {
14-
a, err := aggregate.LoadPatientAggregate(ctx, as, patientID)
20+
return func(ctx context.Context, patientID uuid.UUID, bedID uuid.UUID, expConsistency *uint64) (uint64, *AssignBedConflict, error) {
21+
a, oldState, err := aggregate.LoadPatientAggregateWithSnapshotAt(ctx, as, patientID, expConsistency)
1522
if err != nil {
16-
return 0, err
23+
return 0, nil, err
24+
}
25+
26+
// update happened since
27+
if expConsistency != nil && *expConsistency != a.GetVersion() {
28+
return 0, &AssignBedConflict{
29+
Consistency: a.GetVersion(),
30+
Was: oldState,
31+
Is: a.Patient,
32+
}, nil
1733
}
1834

1935
if err := a.AssignBed(ctx, bedID); err != nil {
20-
return 0, err
36+
return 0, nil, err
2137
}
22-
return as.Save(ctx, a)
38+
39+
consistency, err := as.Save(ctx, a)
40+
return consistency, nil, err
2341
}
2442
}

services/tasks-svc/internal/util/conflict.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,22 @@ import (
1010
// AttributeConflict is a constructor for commonpb.AttributeConflicts
1111
// I'd love to move this somewhere else, but I also don't want common to depend on gen (and thus hwdb, hwes, ...)
1212
func AttributeConflict(is, want proto.Message) (*commonpb.AttributeConflict, error) {
13-
wantAny, err := anypb.New(want)
14-
if err != nil {
15-
return nil, fmt.Errorf("AttributeConflict could not marshal want: %w", err)
13+
var err error
14+
15+
var wantAny *anypb.Any
16+
if want != nil {
17+
wantAny, err = anypb.New(want)
18+
if err != nil {
19+
return nil, fmt.Errorf("AttributeConflict could not marshal want: %w", err)
20+
}
1621
}
17-
isAny, err := anypb.New(is)
18-
if err != nil {
19-
return nil, fmt.Errorf("AttributeConflict could not marshal is: %w", err)
22+
23+
var isAny *anypb.Any
24+
if is != nil {
25+
isAny, err = anypb.New(is)
26+
if err != nil {
27+
return nil, fmt.Errorf("AttributeConflict could not marshal is: %w", err)
28+
}
2029
}
2130

2231
return &commonpb.AttributeConflict{

services/tasks-svc/stories/PatientCRUD_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,103 @@ func TestUpdatePatientConflict(t *testing.T) {
738738

739739
// EXPECT
740740
assert.Equal(t, o.expectConflict, updateRes.Conflict != nil)
741+
if o.expectConflict {
742+
conflict := updateRes.Conflict.ConflictingAttributes["human_readable_identifier"]
743+
assert.NotNil(t, conflict)
744+
exp := "is:{[type.googleapis.com/google.protobuf.StringValue]:{value:\"B\"}} " +
745+
"want:{[type.googleapis.com/google.protobuf.StringValue]:{value:\"C\"}}"
746+
assert.Equal(t, exp, conflict.String())
747+
}
748+
})
749+
}
750+
}
751+
752+
func TestAssignBedConflict(t *testing.T) {
753+
ctx := context.Background()
754+
patientClient := patientServiceClient()
755+
756+
wardId, _ := prepareWard(t, ctx, "")
757+
roomId, _ := prepareRoom(t, ctx, wardId, "")
758+
759+
A, _ := prepareBed(t, ctx, roomId, "A")
760+
B, _ := prepareBed(t, ctx, roomId, "B")
761+
C, _ := prepareBed(t, ctx, roomId, "C")
762+
763+
testMatrix := []struct {
764+
was string
765+
is *string
766+
want string
767+
expectConflict bool
768+
}{
769+
{A, &B, B, false},
770+
{A, &B, C, true},
771+
{A, &A, C, false},
772+
{A, nil, C, true},
773+
}
774+
775+
for i, o := range testMatrix {
776+
t.Run(t.Name()+"_"+strconv.Itoa(i), func(t *testing.T) {
777+
// WAS
778+
patientRes, err := patientClient.CreatePatient(ctx, &pb.CreatePatientRequest{
779+
HumanReadableIdentifier: t.Name(),
780+
Notes: hwutil.PtrTo("A patient for test " + t.Name()),
781+
})
782+
assert.NoError(t, err)
783+
time.Sleep(time.Millisecond * 100)
784+
785+
initialAssignment, err := patientClient.AssignBed(ctx, &pb.AssignBedRequest{
786+
Id: patientRes.Id,
787+
BedId: o.was,
788+
Consistency: &patientRes.Consistency,
789+
})
790+
assert.NoError(t, err)
791+
assert.Nil(t, initialAssignment.Conflict)
792+
793+
id := patientRes.Id
794+
initialConsistency := initialAssignment.Consistency
795+
796+
time.Sleep(time.Millisecond * 100)
797+
798+
// IS
799+
if o.is != nil {
800+
a, err := patientClient.AssignBed(ctx, &pb.AssignBedRequest{
801+
Id: id,
802+
BedId: *o.is,
803+
Consistency: &initialConsistency,
804+
})
805+
assert.NoError(t, err)
806+
assert.Nil(t, a.Conflict)
807+
} else {
808+
u, err := patientClient.UnassignBed(ctx, &pb.UnassignBedRequest{
809+
Id: id,
810+
Consistency: &initialConsistency,
811+
})
812+
assert.NoError(t, err)
813+
assert.Nil(t, u.Conflict)
814+
}
815+
time.Sleep(time.Millisecond * 100)
816+
817+
// WANT
818+
updateRes, err := patientClient.AssignBed(ctx, &pb.AssignBedRequest{
819+
Id: id,
820+
BedId: o.want,
821+
Consistency: &initialConsistency,
822+
})
823+
assert.NoError(t, err)
824+
825+
// EXPECT
826+
assert.Equal(t, o.expectConflict, updateRes.Conflict != nil)
827+
if o.expectConflict {
828+
conflict := updateRes.Conflict.ConflictingAttributes["bed_id"]
829+
assert.NotNil(t, conflict)
830+
831+
exp := "want:{[type.googleapis.com/google.protobuf.StringValue]:{value:\"" + o.want + "\"}}"
832+
if o.is != nil {
833+
exp = "is:{[type.googleapis.com/google.protobuf.StringValue]:{value:\"" + *o.is + "\"}} " +
834+
"want:{[type.googleapis.com/google.protobuf.StringValue]:{value:\"" + o.want + "\"}}"
835+
}
836+
assert.Equal(t, exp, conflict.String())
837+
}
741838
})
742839
}
743840
}

0 commit comments

Comments
 (0)