@@ -3,15 +3,22 @@ package api
33import (
44 "common"
55 "context"
6+ "fmt"
7+ commonpb "gen/libs/common/v1"
68 pb "gen/services/tasks_svc/v1"
79 "github.com/google/uuid"
10+ zlog "github.com/rs/zerolog"
811 "google.golang.org/grpc/codes"
912 "google.golang.org/grpc/status"
13+ "google.golang.org/protobuf/proto"
1014 "google.golang.org/protobuf/types/known/timestamppb"
15+ "google.golang.org/protobuf/types/known/wrapperspb"
1116 "hwes"
1217 "hwutil"
1318 "strconv"
1419 "tasks-svc/internal/task/handlers"
20+ "tasks-svc/internal/util"
21+ "time"
1522)
1623
1724type TaskGrpcService struct {
@@ -61,15 +68,114 @@ func (s *TaskGrpcService) CreateTask(ctx context.Context, req *pb.CreateTaskRequ
6168 }, nil
6269}
6370
71+ func timeAlreadyUpdated (was , is * time.Time ) bool {
72+ if was != nil && is != nil {
73+ return ! was .Round (time .Second ).Equal (is .Round (time .Second ))
74+ }
75+
76+ return true
77+ }
78+
6479func (s * TaskGrpcService ) UpdateTask (ctx context.Context , req * pb.UpdateTaskRequest ) (* pb.UpdateTaskResponse , error ) {
80+ log := zlog .Ctx (ctx )
81+
6582 taskID , err := uuid .Parse (req .GetId ())
6683 if err != nil {
6784 return nil , err
6885 }
6986
70- consistency , err := s .handlers .Commands .V1 .UpdateTask (ctx , taskID , req .Name , req .Description , req .Status , req .Public , req .DueAt )
71- if err != nil {
72- return nil , err
87+ expConsistency , ok := hwutil .ParseConsistency (req .Consistency )
88+ if ! ok {
89+ return nil , common .UnparsableConsistencyError (ctx , "consistency" )
90+ }
91+
92+ var consistency uint64
93+
94+ for i := 0 ; true ; i ++ {
95+ if i > 10 {
96+ log .Warn ().Msg ("UpdatePatient: conflict circuit breaker triggered" )
97+ return nil , fmt .Errorf ("failed conflict resolution" )
98+ }
99+
100+ c , conflict , err := s .handlers .Commands .V1 .UpdateTask (ctx ,
101+ taskID , req .Name , req .Description , req .Status , req .Public , req .DueAt , expConsistency )
102+ if err != nil {
103+ return nil , err
104+ }
105+ consistency = c
106+
107+ if conflict == nil {
108+ break
109+ }
110+ conflicts := make (map [string ]* commonpb.AttributeConflict )
111+
112+ // TODO: find a generic approach
113+ nameUpdateRequested := req .Name != nil && * req .Name != conflict .Is .Name
114+ nameAlreadyUpdated := conflict .Was .Name != conflict .Is .Name
115+ if nameUpdateRequested && nameAlreadyUpdated {
116+ conflicts ["name" ], err = util .AttributeConflict (
117+ wrapperspb .String (conflict .Is .Name ),
118+ wrapperspb .String (* req .Name ),
119+ )
120+ if err != nil {
121+ return nil , err
122+ }
123+ }
124+
125+ descrUpdateRequested := req .Description != nil && * req .Description != conflict .Is .Description
126+ descrAlreadyUpdated := conflict .Was .Description != conflict .Is .Description
127+ if descrUpdateRequested && descrAlreadyUpdated {
128+ conflicts ["description" ], err = util .AttributeConflict (
129+ wrapperspb .String (conflict .Is .Description ),
130+ wrapperspb .String (* req .Description ),
131+ )
132+ if err != nil {
133+ return nil , fmt .Errorf ("could not marshall description conflict: %w" , err )
134+ }
135+ }
136+
137+ dueUpdateRequested := req .DueAt != nil &&
138+ (conflict .Is .DueAt == nil || ! req .DueAt .AsTime ().Round (time .Second ).Equal (conflict .Is .DueAt .Round (time .Second )))
139+ dueAlreadyUpdated := timeAlreadyUpdated (conflict .Was .DueAt , conflict .Is .DueAt )
140+ if dueUpdateRequested && dueAlreadyUpdated {
141+ var is proto.Message = nil
142+ if conflict .Is .DueAt != nil {
143+ is = timestamppb .New (* conflict .Is .DueAt )
144+ }
145+ conflicts ["due_at" ], err = util .AttributeConflict (
146+ is ,
147+ req .DueAt ,
148+ )
149+ if err != nil {
150+ return nil , fmt .Errorf ("could not marshall due_at conflict: %w" , err )
151+ }
152+ }
153+
154+ statusUpdateRequested := req .Status != nil && * req .Status != conflict .Is .Status
155+ statusAlreadyUpdated := conflict .Was .Status != conflict .Is .Status
156+ if statusUpdateRequested && statusAlreadyUpdated {
157+ conflicts ["status" ], err = util .AttributeConflict (
158+ wrapperspb .Int32 (int32 (conflict .Is .Status )),
159+ wrapperspb .Int32 (int32 (* req .Status )),
160+ )
161+ if err != nil {
162+ return nil , fmt .Errorf ("could not marshall status conflict: %w" , err )
163+ }
164+ }
165+
166+ // bool public can never cause a problem
167+ // the user expects public = B, and sets it to \neg B
168+ // so either that is the case still, or the update will do nothing anyway
169+
170+ if len (conflicts ) != 0 {
171+ return & pb.UpdateTaskResponse {
172+ Conflict : & commonpb.Conflict {ConflictingAttributes : conflicts },
173+ Consistency : strconv .FormatUint (conflict .Consistency , 10 ),
174+ }, nil
175+ }
176+
177+ // no conflict? retry with new consistency
178+ expConsistency = & conflict .Consistency
73179 }
74180
75181 return & pb.UpdateTaskResponse {
0 commit comments