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

Commit 9b6bab1

Browse files
committed
ward conflict
1 parent 2be7537 commit 9b6bab1

File tree

7 files changed

+197
-13
lines changed

7 files changed

+197
-13
lines changed

libs/common/errors.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package common
2+
3+
import (
4+
"common/locale"
5+
"context"
6+
"google.golang.org/genproto/googleapis/rpc/errdetails"
7+
"google.golang.org/grpc/codes"
8+
"hwlocale"
9+
)
10+
11+
// SingleInvalidFieldError builds a NewStatusError
12+
func SingleInvalidFieldError(ctx context.Context, field, msg string, locale hwlocale.Locale) error {
13+
return NewStatusError(ctx,
14+
codes.InvalidArgument,
15+
msg,
16+
locale,
17+
&errdetails.BadRequest{
18+
FieldViolations: []*errdetails.BadRequest_FieldViolation{
19+
{
20+
Field: field,
21+
Description: hwlocale.Localize(ctx, locale),
22+
},
23+
}},
24+
)
25+
}
26+
27+
func UnparsableConsistencyError(ctx context.Context, field string) error {
28+
return SingleInvalidFieldError(ctx, field, "consistency not parsable", locale.InvalidFieldError(ctx))
29+
}

libs/hwutil/parse.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,14 @@ type JSONAble interface {
131131
type MapAble interface {
132132
ToMap() map[string]interface{}
133133
}
134+
135+
func ParseConsistency(consistencyStr *string) (consistency *uint64, success bool) {
136+
if consistencyStr == nil {
137+
return nil, true
138+
}
139+
if c, err := strconv.ParseUint(*consistencyStr, 10, 64); err != nil {
140+
return nil, false
141+
} else {
142+
return &c, true
143+
}
144+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package util
2+
3+
import (
4+
"fmt"
5+
commonpb "gen/libs/common/v1"
6+
"google.golang.org/protobuf/proto"
7+
"google.golang.org/protobuf/types/known/anypb"
8+
)
9+
10+
// AttributeConflict is a constructor for commonpb.AttributeConflicts
11+
// I'd love to move this somewhere else, but I also don't want common to depend on gen (and thus hwdb, hwes, ...)
12+
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)
16+
}
17+
isAny, err := anypb.New(is)
18+
if err != nil {
19+
return nil, fmt.Errorf("AttributeConflict could not marshal is: %w", err)
20+
}
21+
22+
return &commonpb.AttributeConflict{
23+
Is: isAny,
24+
Want: wantAny,
25+
}, nil
26+
}

services/tasks-svc/internal/ward/ward.go

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
package ward
22

33
import (
4+
"common"
45
"context"
6+
commonpb "gen/libs/common/v1"
57
pb "gen/services/tasks_svc/v1"
68
"github.com/google/uuid"
79
zlog "github.com/rs/zerolog/log"
810
"google.golang.org/grpc/codes"
911
"google.golang.org/grpc/status"
12+
"google.golang.org/protobuf/types/known/wrapperspb"
1013
"hwdb"
1114
"hwutil"
1215
"strconv"
1316
"tasks-svc/internal/tracking"
17+
"tasks-svc/internal/util"
1418
"tasks-svc/repos/ward_repo"
1519
)
1620

@@ -140,16 +144,28 @@ func (ServiceServer) GetRecentWards(ctx context.Context, req *pb.GetRecentWardsR
140144
}
141145

142146
func (ServiceServer) UpdateWard(ctx context.Context, req *pb.UpdateWardRequest) (*pb.UpdateWardResponse, error) {
143-
wardRepo := ward_repo.New(hwdb.GetDB())
144-
145147
// TODO: Auth
146148

147149
id, err := uuid.Parse(req.Id)
148150
if err != nil {
149151
return nil, status.Error(codes.InvalidArgument, err.Error())
150152
}
151153

152-
consistency, err := wardRepo.UpdateWard(ctx, ward_repo.UpdateWardParams{
154+
expConsistency, ok := hwutil.ParseConsistency(req.Consistency)
155+
if !ok {
156+
return nil, common.UnparsableConsistencyError(ctx, "consistency")
157+
}
158+
159+
// Start TX
160+
tx, rollback, err := hwdb.BeginTx(hwdb.GetDB(), ctx)
161+
if err != nil {
162+
return nil, err
163+
}
164+
defer rollback()
165+
wardRepo := ward_repo.New(tx)
166+
167+
// Do Update
168+
result, err := wardRepo.UpdateWard(ctx, ward_repo.UpdateWardParams{
153169
ID: id,
154170
Name: req.Name,
155171
})
@@ -158,11 +174,46 @@ func (ServiceServer) UpdateWard(ctx context.Context, req *pb.UpdateWardRequest)
158174
return nil, err
159175
}
160176

177+
// conflict detection
178+
if expConsistency != nil && *expConsistency != uint64(result.OldConsistency) {
179+
conflicts := make(map[string]*commonpb.AttributeConflict)
180+
181+
// wards are not event-sourced, we thus don't have information on what has changed since
182+
// however, wards (currently) only have one field: Name, thus it must have been changed
183+
if req.Name != nil {
184+
conflicts["name"], err = util.AttributeConflict(
185+
wrapperspb.String(result.OldName),
186+
wrapperspb.String(*req.Name),
187+
)
188+
if err != nil {
189+
return nil, err
190+
}
191+
}
192+
193+
if len(conflicts) != 0 {
194+
// prevent the update
195+
if err := hwdb.Error(ctx, tx.Rollback(ctx)); err != nil {
196+
return nil, err
197+
}
198+
199+
// return conflict
200+
return &pb.UpdateWardResponse{
201+
Conflict: &commonpb.Conflict{ConflictingAttributes: conflicts},
202+
Consistency: strconv.FormatUint(uint64(result.OldConsistency), 10),
203+
}, nil
204+
}
205+
}
206+
207+
// Commit Update
208+
if err := hwdb.Error(ctx, tx.Commit(ctx)); err != nil {
209+
return nil, err
210+
}
211+
161212
tracking.AddWardToRecentActivity(ctx, id.String())
162213

163214
return &pb.UpdateWardResponse{
164-
Conflict: nil, // TODO
165-
Consistency: strconv.FormatUint(uint64(consistency), 10),
215+
Conflict: nil,
216+
Consistency: strconv.FormatUint(uint64(result.Consistency), 10),
166217
}, nil
167218
}
168219

services/tasks-svc/repos/ward_repo.sql

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,19 @@ SELECT EXISTS (
5555
) ward_exists;
5656

5757
-- name: UpdateWard :one
58+
WITH old_table AS (
59+
SELECT name as old_name, consistency as old_consistency
60+
FROM wards
61+
WHERE wards.id = @id
62+
)
5863
UPDATE wards
5964
SET name = coalesce(sqlc.narg('name'), name),
6065
consistency = consistency + 1
61-
WHERE id = @id
62-
RETURNING consistency;
66+
WHERE wards.id = @id
67+
RETURNING
68+
consistency,
69+
(SELECT old_name FROM old_table),
70+
(SELECT old_consistency FROM old_table);
6371

6472

6573
-- name: DeleteWard :exec

services/tasks-svc/repos/ward_repo/ward_repo.sql.go

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

services/tasks-svc/stories/WardCRUD_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
pb "gen/services/tasks_svc/v1"
66
"github.com/google/uuid"
77
"github.com/stretchr/testify/assert"
8+
"google.golang.org/protobuf/types/known/wrapperspb"
89
"hwtesting"
910
"hwutil"
1011
"strconv"
@@ -377,3 +378,47 @@ func TestGetWardDetails(t *testing.T) {
377378
assert.Equal(t, expected, actual)
378379

379380
}
381+
382+
func TestUpdateWardConflict(t *testing.T) {
383+
ctx := context.Background()
384+
wardClient := wardServiceClient()
385+
386+
// prepare
387+
wardId, initialConsistency := prepareWard(t, ctx, "")
388+
389+
name1 := "This came first"
390+
391+
// update 1
392+
update1Res, err := wardClient.UpdateWard(ctx, &pb.UpdateWardRequest{
393+
Id: wardId,
394+
Name: &name1,
395+
Consistency: &initialConsistency,
396+
})
397+
assert.NoError(t, err)
398+
assert.Nil(t, update1Res.Conflict)
399+
assert.NotEqual(t, initialConsistency, update1Res.Consistency)
400+
401+
name2 := "This came second"
402+
403+
// racing update 2
404+
update2Res, err := wardClient.UpdateWard(ctx, &pb.UpdateWardRequest{
405+
Id: wardId,
406+
Name: &name2,
407+
Consistency: &initialConsistency,
408+
})
409+
assert.NoError(t, err)
410+
assert.Equal(t, update1Res.Consistency, update2Res.Consistency)
411+
assert.NotNil(t, update2Res.Conflict)
412+
413+
nameRes := update2Res.Conflict.ConflictingAttributes["name"]
414+
assert.NotNil(t, nameRes)
415+
416+
nameIs := &wrapperspb.StringValue{}
417+
assert.NoError(t, nameRes.Is.UnmarshalTo(nameIs))
418+
assert.Equal(t, name1, nameIs.Value)
419+
420+
nameWant := &wrapperspb.StringValue{}
421+
assert.NoError(t, nameRes.Want.UnmarshalTo(nameWant))
422+
assert.Equal(t, name2, nameWant.Value)
423+
424+
}

0 commit comments

Comments
 (0)