Skip to content

Commit 331cdb2

Browse files
authored
Support container locks (#758)
2 parents ab40d4a + 4205c88 commit 331cdb2

File tree

16 files changed

+229
-8
lines changed

16 files changed

+229
-8
lines changed

client/client_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1074,7 +1074,7 @@ func testStatusResponses[SRV interface {
10741074
1023,
10751075
1036,
10761076
2055,
1077-
3074,
1077+
3075,
10781078
4098,
10791079
} {
10801080
t.Run("unrecognized_"+strconv.FormatUint(uint64(code), 10), func(t *testing.T) {

client/container.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ func (x *PrmContainerDelete) AttachSignature(sig neofscrypto.Signature) {
388388
// Reflects all internal errors in second return value (transport problems, response processing, etc.).
389389
// Return errors:
390390
// - [ErrMissingSigner]
391+
// - [apistatus.ErrContainerLocked]
391392
func (c *Client) ContainerDelete(ctx context.Context, id cid.ID, signer neofscrypto.Signer, prm PrmContainerDelete) error {
392393
var err error
393394
if c.prm.statisticCallback != nil {

client/container_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"testing"
1010
"time"
1111

12+
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
1213
"github.com/nspcc-dev/neofs-sdk-go/container"
1314
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
1415
cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test"
@@ -24,6 +25,7 @@ import (
2425
protonetmap "github.com/nspcc-dev/neofs-sdk-go/proto/netmap"
2526
protorefs "github.com/nspcc-dev/neofs-sdk-go/proto/refs"
2627
protosession "github.com/nspcc-dev/neofs-sdk-go/proto/session"
28+
protostatus "github.com/nspcc-dev/neofs-sdk-go/proto/status"
2729
"github.com/nspcc-dev/neofs-sdk-go/session"
2830
sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/test"
2931
"github.com/nspcc-dev/neofs-sdk-go/stat"
@@ -1409,6 +1411,25 @@ func TestClient_ContainerDelete(t *testing.T) {
14091411
}
14101412
})
14111413
t.Run("statuses", func(t *testing.T) {
1414+
t.Run("locked", func(t *testing.T) {
1415+
srv := newTestDeleteContainerServer()
1416+
c := newTestContainerClient(t, srv)
1417+
1418+
st := protostatus.Status{Code: 3074}
1419+
1420+
srv.respondWithStatus(&st)
1421+
1422+
err := c.ContainerDelete(ctx, anyID, anyValidSigner, anyValidOpts)
1423+
require.ErrorIs(t, err, apistatus.ErrContainerLocked)
1424+
require.EqualError(t, err, "status: code = 3074 message = container is locked")
1425+
1426+
st.Message = "some lock context"
1427+
1428+
err = c.ContainerDelete(ctx, anyID, anyValidSigner, anyValidOpts)
1429+
require.ErrorIs(t, err, apistatus.ErrContainerLocked)
1430+
require.EqualError(t, err, "status: code = 3074 message = some lock context")
1431+
})
1432+
14121433
testStatusResponses(t, newTestDeleteContainerServer, newTestContainerClient, func(c *Client) error {
14131434
return c.ContainerDelete(ctx, anyID, anyValidSigner, anyValidOpts)
14141435
})

client/status/container.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ var (
1313
// ErrContainerNotFound is an instance of ContainerNotFound error status. It's expected to be used for [errors.Is]
1414
// and MUST NOT be changed.
1515
ErrContainerNotFound ContainerNotFound
16+
// ErrContainerLocked is an instance of ContainerLocked error status. It's expected to be used for [errors.Is]
17+
// and MUST NOT be changed.
18+
ErrContainerLocked ContainerLocked
1619
)
1720

1821
// ContainerNotFound describes status of the failure because of the missing container.
@@ -95,3 +98,49 @@ func (x EACLNotFound) protoMessage() *protostatus.Status {
9598
}
9699
return &protostatus.Status{Code: protostatus.EACLNotFound, Message: x.msg, Details: x.dts}
97100
}
101+
102+
// ContainerLocked describes status of the failure because of the locked container.
103+
type ContainerLocked struct {
104+
msg string
105+
dts []*protostatus.Status_Detail
106+
}
107+
108+
// NewContainerLocked constructs ContainerLocked with given message.
109+
func NewContainerLocked(msg string) ContainerLocked {
110+
return ContainerLocked{msg: msg}
111+
}
112+
113+
const defaultContainerLockedMsg = "container is locked"
114+
115+
// Error implements built-in [error] interface.
116+
func (x ContainerLocked) Error() string {
117+
if x.msg == "" {
118+
x.msg = defaultContainerLockedMsg
119+
}
120+
121+
return errMessageStatus(protostatus.ContainerLocked, x.msg)
122+
}
123+
124+
// Is implements interface for correct checking current error type with [errors.Is].
125+
func (x ContainerLocked) Is(target error) bool {
126+
switch target.(type) {
127+
default:
128+
return errors.Is(Error, target)
129+
case ContainerLocked, *ContainerLocked:
130+
return true
131+
}
132+
}
133+
134+
// implements local interface defined in [ToError] func.
135+
func (x *ContainerLocked) fromProtoMessage(st *protostatus.Status) {
136+
x.msg = st.Message
137+
x.dts = st.Details
138+
}
139+
140+
// implements local interface defined in [FromError] func.
141+
func (x ContainerLocked) protoMessage() *protostatus.Status {
142+
if x.msg == "" {
143+
x.msg = defaultContainerLockedMsg
144+
}
145+
return &protostatus.Status{Code: protostatus.ContainerLocked, Message: x.msg, Details: x.dts}
146+
}

client/status/container_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package apistatus_test
2+
3+
import (
4+
"testing"
5+
6+
apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestNewContainerLocked(t *testing.T) {
11+
var e apistatus.ContainerLocked
12+
require.EqualError(t, e, "status: code = 3074 message = container is locked")
13+
14+
e = apistatus.NewContainerLocked("some message")
15+
require.EqualError(t, e, "status: code = 3074 message = some message")
16+
}

client/status/errors_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,10 @@ func TestErrors(t *testing.T) {
7676
errs: []error{EACLNotFound{}, new(EACLNotFound)},
7777
errVariable: ErrEACLNotFound,
7878
},
79-
79+
{
80+
errs: []error{ContainerLocked{}, new(ContainerLocked)},
81+
errVariable: ErrContainerLocked,
82+
},
8083
{
8184
errs: []error{SessionTokenExpired{}, new(SessionTokenExpired)},
8285
errVariable: ErrSessionTokenExpired,

client/status/v2.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
// Container failures:
3434
// - [protostatus.ContainerNotFound]: *[ContainerNotFound];
3535
// - [protostatus.EACLNotFound]: *[EACLNotFound];
36+
// - [protostatus.ContainerLocked]: *[ContainerLocked];
3637
//
3738
// Session failures:
3839
// - [protostatus.SessionTokenNotFound]: *[SessionTokenNotFound];
@@ -84,6 +85,8 @@ func ToError(st *protostatus.Status) error {
8485
decoder = new(ContainerNotFound)
8586
case protostatus.EACLNotFound:
8687
decoder = new(EACLNotFound)
88+
case protostatus.ContainerLocked:
89+
decoder = new(ContainerLocked)
8790
case protostatus.SessionTokenNotFound:
8891
decoder = new(SessionTokenNotFound)
8992
case protostatus.SessionTokenExpired:

client/status/v2_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,17 @@ func TestToError(t *testing.T) {
203203
return errors.As(err, &target)
204204
},
205205
},
206+
{
207+
new: func() error {
208+
return new(apistatus.ContainerLocked)
209+
},
210+
code: 3074,
211+
compatibleErrs: []error{apistatus.ErrContainerLocked, apistatus.ContainerLocked{}, &apistatus.ContainerLocked{}, apistatus.Error},
212+
checkAsErr: func(err error) bool {
213+
var target *apistatus.ContainerLocked
214+
return errors.As(err, &target)
215+
},
216+
},
206217
{
207218
new: func() error {
208219
return new(apistatus.SessionTokenNotFound)

container/container.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const (
2626
sysAttrDisableHomohash = sysAttrPrefix + "DISABLE_HOMOMORPHIC_HASHING"
2727
sysAttrDomainName = sysAttrPrefix + "NAME"
2828
sysAttrDomainZone = sysAttrPrefix + "ZONE"
29+
sysAttrLockUntil = sysAttrPrefix + "LOCK_UNTIL"
2930
)
3031

3132
// Container represents descriptor of the NeoFS container. Container logically
@@ -168,7 +169,7 @@ func (x *Container) fromProtoMessage(m *protocontainer.Container, checkFieldPres
168169
}
169170

170171
switch key {
171-
case attributeTimestamp:
172+
case attributeTimestamp, sysAttrLockUntil:
172173
_, err = strconv.ParseInt(val, 10, 64)
173174
}
174175

@@ -542,3 +543,31 @@ func (x Container) Version() version.Version {
542543
}
543544
return version.Version{}
544545
}
546+
547+
// SetLockUntil sets attribute with removal lock timestamp in Unix Timestamp
548+
// format.
549+
func (x *Container) SetLockUntil(until time.Time) {
550+
x.SetAttribute(sysAttrLockUntil, strconv.FormatInt(until.Unix(), 10))
551+
}
552+
553+
// GetLockUntil looks up for attribute with removal lock timestamp in Unix
554+
// Timestamp format. If attribute is missing, GetLockUntil returns both zero.
555+
// Otherwise, GetLockUntil parses the value. If parsing fails, GetLockUntil
556+
// returns an error containing the value.
557+
func (x Container) GetLockUntil() (time.Time, error) {
558+
attr := x.Attribute(sysAttrLockUntil)
559+
if attr == "" {
560+
return time.Time{}, nil
561+
}
562+
563+
n, err := strconv.ParseInt(attr, 10, 64)
564+
if err != nil {
565+
var ne *strconv.NumError
566+
if !errors.As(err, &ne) {
567+
panic(fmt.Sprintf("unexpected strconv.ParseInt error type %T", err))
568+
}
569+
return time.Time{}, fmt.Errorf("parse %q: %w", ne.Num, ne.Err)
570+
}
571+
572+
return time.Unix(n, 0), nil
573+
}

container/container_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,3 +1003,54 @@ func TestContainer_VerifySignature(t *testing.T) {
10031003
require.False(t, validContainer.VerifySignature(sig), i)
10041004
}
10051005
}
1006+
1007+
func TestContainer_SetLockUntil(t *testing.T) {
1008+
var cnr container.Container
1009+
1010+
got, err := cnr.GetLockUntil()
1011+
require.NoError(t, err)
1012+
require.Zero(t, got)
1013+
1014+
until := time.Unix(time.Now().Unix(), 0)
1015+
1016+
cnr.SetLockUntil(until)
1017+
1018+
got, err = cnr.GetLockUntil()
1019+
require.NoError(t, err)
1020+
require.True(t, got.Equal(until))
1021+
1022+
until = until.Add(5 * time.Second)
1023+
1024+
cnr.SetLockUntil(until)
1025+
1026+
got, err = cnr.GetLockUntil()
1027+
require.NoError(t, err)
1028+
require.True(t, got.Equal(until))
1029+
}
1030+
1031+
func TestContainer_GetLockUntil(t *testing.T) {
1032+
var cnr container.Container
1033+
1034+
got, err := cnr.GetLockUntil()
1035+
require.NoError(t, err)
1036+
require.Zero(t, got)
1037+
1038+
cnr.SetAttribute("__NEOFS__LOCK_UNTIL", "foo")
1039+
_, err = cnr.GetLockUntil()
1040+
require.EqualError(t, err, `parse "foo": invalid syntax`)
1041+
1042+
for _, tc := range []struct {
1043+
s string
1044+
n int64
1045+
}{
1046+
{s: "1234567890", n: 1234567890},
1047+
{s: "0", n: 0},
1048+
{s: "-42", n: -42},
1049+
} {
1050+
cnr.SetAttribute("__NEOFS__LOCK_UNTIL", tc.s)
1051+
1052+
got, err = cnr.GetLockUntil()
1053+
require.NoError(t, err)
1054+
require.True(t, got.Equal(time.Unix(tc.n, 0)))
1055+
}
1056+
}

0 commit comments

Comments
 (0)