From 00b2b77b261b195df583916660161d45df4a30ae Mon Sep 17 00:00:00 2001
From: Anton Nikiforov <an.nikiforov@yadro.com>
Date: Thu, 2 May 2024 14:29:59 +0300
Subject: [PATCH] [#1112] node: Implement `Range\RangeHash` requests for EC
 object

Signed-off-by: Anton Nikiforov <an.nikiforov@yadro.com>
---
 go.mod                                        |  4 +-
 go.sum                                        |  8 +--
 pkg/local_object_storage/engine/range.go      | 13 ++++
 pkg/services/object/get/assemblerec.go        | 62 ++++++++++++++++---
 .../object/get/v2/get_range_forwarder.go      |  3 +
 pkg/services/object/get/v2/service.go         |  3 +
 pkg/services/object/get/v2/util.go            | 11 ++++
 7 files changed, 88 insertions(+), 16 deletions(-)

diff --git a/go.mod b/go.mod
index 1be2526f20..08a59581d7 100644
--- a/go.mod
+++ b/go.mod
@@ -4,10 +4,10 @@ go 1.21
 
 require (
 	code.gitea.io/sdk/gitea v0.17.1
-	git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240422151450-df9b65324a4c
+	git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240427200446-67c6f305b21f
 	git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.0
 	git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20231101111734-b3ad3335ff65
-	git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240424080726-20ab57bf7ec3
+	git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240507063414-99e02858af12
 	git.frostfs.info/TrueCloudLab/hrw v1.2.1
 	git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240412130734-0e69e485115a
 	git.frostfs.info/TrueCloudLab/tzhash v1.8.0
diff --git a/go.sum b/go.sum
index 0795c955b7..6200022c71 100644
--- a/go.sum
+++ b/go.sum
@@ -1,15 +1,15 @@
 code.gitea.io/sdk/gitea v0.17.1 h1:3jCPOG2ojbl8AcfaUCRYLT5MUcBMFwS0OSK2mA5Zok8=
 code.gitea.io/sdk/gitea v0.17.1/go.mod h1:aCnBqhHpoEWA180gMbaCtdX9Pl6BWBAuuP2miadoTNM=
-git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240422151450-df9b65324a4c h1:RFDrNsF2e+EJfaB8lZrRRxNjQkLfM09gnEyudvGuc10=
-git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240422151450-df9b65324a4c/go.mod h1:OBDSr+DqV1z4VDouoX3YMleNc4DPBVBWTG3WDT2PK1o=
+git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240427200446-67c6f305b21f h1:YyjsQNtrngQzIKOUtApXoi5r5pewatM+cXfpY19vZWo=
+git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240427200446-67c6f305b21f/go.mod h1:OBDSr+DqV1z4VDouoX3YMleNc4DPBVBWTG3WDT2PK1o=
 git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.0 h1:FzurjElUwC7InY9v5rzXReKbfBL5yRJKSWJPq6BKhH0=
 git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.0/go.mod h1:F/fe1OoIDKr5Bz99q4sriuHDuf3aZefZy9ZsCqEtgxc=
 git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0 h1:FxqFDhQYYgpe41qsIHVOcdzSVCB8JNSfPG7Uk4r2oSk=
 git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0/go.mod h1:RUIKZATQLJ+TaYQa60X2fTDwfuhMfm8Ar60bQ5fr+vU=
 git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20231101111734-b3ad3335ff65 h1:PaZ8GpnUoXxUoNsc1qp36bT2u7FU+neU4Jn9cl8AWqI=
 git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20231101111734-b3ad3335ff65/go.mod h1:6aAX80dvJ3r5fjN9CzzPglRptoiPgIC9KFGGsUA+1Hw=
-git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240424080726-20ab57bf7ec3 h1:7Sd/J2IM0uGpmFKBgseUh6/JsdJN06b8W8UZMKAUDZg=
-git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240424080726-20ab57bf7ec3/go.mod h1:wDFmMP7l00Xd5VZVzF2MuhyJCnotyhfxHYnvrEEG/e4=
+git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240507063414-99e02858af12 h1:uEAn+TBXxgqXXygrYW2X8dFmGydGyJvBc5ysnfHFhYM=
+git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240507063414-99e02858af12/go.mod h1:e7H9nNFpx1Tj3R20Zoxy0Vo6Srlb6zV5L7ZQXqg9rn4=
 git.frostfs.info/TrueCloudLab/hrw v1.2.1 h1:ccBRK21rFvY5R1WotI6LNoPlizk7qSvdfD8lNIRudVc=
 git.frostfs.info/TrueCloudLab/hrw v1.2.1/go.mod h1:C1Ygde2n843yTZEQ0FP69jYiuaYV0kriLvP4zm8JuvM=
 git.frostfs.info/TrueCloudLab/policy-engine v0.0.0-20240412130734-0e69e485115a h1:wbndKvHbwDQiSMQWL75RxiTZCeUyCi7NUj1lsfdAGkc=
diff --git a/pkg/local_object_storage/engine/range.go b/pkg/local_object_storage/engine/range.go
index e45845d6c2..f5b33a251e 100644
--- a/pkg/local_object_storage/engine/range.go
+++ b/pkg/local_object_storage/engine/range.go
@@ -102,6 +102,9 @@ func (e *StorageEngine) getRange(ctx context.Context, prm RngPrm) (RngRes, error
 	if it.SplitInfo != nil {
 		return RngRes{}, logicerr.Wrap(objectSDK.NewSplitInfoError(it.SplitInfo))
 	}
+	if it.ECInfo != nil {
+		return RngRes{}, logicerr.Wrap(objectSDK.NewECInfoError(it.ECInfo))
+	}
 
 	if it.Object == nil {
 		// If any shard is in a degraded mode, we should assume that metabase could store
@@ -147,6 +150,8 @@ type getRangeShardIterator struct {
 	Object         *objectSDK.Object
 	SplitInfoError *objectSDK.SplitInfoError
 	SplitInfo      *objectSDK.SplitInfo
+	ECInfoError    *objectSDK.ECInfoError
+	ECInfo         *objectSDK.ECInfo
 	OutError       error
 	ShardWithMeta  hashedShard
 	MetaError      error
@@ -188,6 +193,14 @@ func (i *getRangeShardIterator) tryGetWithMeta(ctx context.Context) {
 
 			// stop iterating over shards if SplitInfo structure is complete
 			return withLink && withLast
+		case errors.As(err, &i.ECInfoError):
+			if i.ECInfo == nil {
+				i.ECInfo = objectSDK.NewECInfo()
+			}
+
+			util.MergeECInfo(i.ECInfoError.ECInfo(), i.ECInfo)
+			// stop iterating over shards if ECInfo structure is complete
+			return len(i.ECInfo.Chunks) == int(i.ECInfo.Chunks[0].Total)
 		case
 			client.IsErrObjectAlreadyRemoved(err),
 			shard.IsErrOutOfRange(err):
diff --git a/pkg/services/object/get/assemblerec.go b/pkg/services/object/get/assemblerec.go
index d73d771cf8..4a624e4676 100644
--- a/pkg/services/object/get/assemblerec.go
+++ b/pkg/services/object/get/assemblerec.go
@@ -8,6 +8,7 @@ import (
 	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
 	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/policy"
 	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger"
+	apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
 	objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
 	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/erasurecode"
 	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
@@ -45,23 +46,64 @@ func newAssemblerEC(
 // Assemble assembles erasure-coded object and writes it's content to ObjectWriter.
 // It returns parent object.
 func (a *assemblerec) Assemble(ctx context.Context, writer ObjectWriter, headOnly bool) (*objectSDK.Object, error) {
-	parts := a.retrieveParts(ctx, headOnly)
+	if headOnly {
+		return a.reconstructHeader(ctx, writer)
+	} else if a.rng != nil {
+		return a.reconstructRange(ctx, writer)
+	}
+	return a.reconstructObject(ctx, writer)
+}
+
+func (a *assemblerec) getConstructor() (*erasurecode.Constructor, error) {
 	cnt, err := a.cs.Get(a.addr.Container())
 	if err != nil {
 		return nil, err
 	}
-	c, err := erasurecode.NewConstructor(
-		policy.ECDataCount(cnt.Value.PlacementPolicy()),
-		policy.ECParityCount(cnt.Value.PlacementPolicy()),
-	)
+	dataCount := policy.ECDataCount(cnt.Value.PlacementPolicy())
+	parityCount := policy.ECParityCount(cnt.Value.PlacementPolicy())
+	return erasurecode.NewConstructor(dataCount, parityCount)
+}
+
+func (a *assemblerec) reconstructHeader(ctx context.Context, writer ObjectWriter) (*objectSDK.Object, error) {
+	parts := a.retrieveParts(ctx, true)
+	c, err := a.getConstructor()
 	if err != nil {
 		return nil, err
 	}
-	if headOnly {
-		obj, err := c.ReconstructHeader(parts)
-		if err == nil {
-			return obj, writer.WriteHeader(ctx, obj)
-		}
+	obj, err := c.ReconstructHeader(parts)
+	if err == nil {
+		return obj, writer.WriteHeader(ctx, obj)
+	}
+	return nil, err
+}
+
+func (a *assemblerec) reconstructRange(ctx context.Context, writer ObjectWriter) (*objectSDK.Object, error) {
+	parts := a.retrieveParts(ctx, false)
+	c, err := a.getConstructor()
+	if err != nil {
+		return nil, err
+	}
+	obj, err := c.Reconstruct(parts)
+	if err != nil {
+		return nil, err
+	}
+
+	from := a.rng.GetOffset()
+	to := from + a.rng.GetLength()
+	if pLen := uint64(len(obj.Payload())); to < from || pLen < from || pLen < to {
+		return nil, &apistatus.ObjectOutOfRange{}
+	}
+	err = writer.WriteChunk(ctx, obj.Payload()[from:to])
+	if err != nil {
+		return nil, err
+	}
+	return obj, err
+}
+
+func (a *assemblerec) reconstructObject(ctx context.Context, writer ObjectWriter) (*objectSDK.Object, error) {
+	parts := a.retrieveParts(ctx, false)
+	c, err := a.getConstructor()
+	if err != nil {
 		return nil, err
 	}
 	obj, err := c.Reconstruct(parts)
diff --git a/pkg/services/object/get/v2/get_range_forwarder.go b/pkg/services/object/get/v2/get_range_forwarder.go
index 8a56c59a6a..5b05ec3703 100644
--- a/pkg/services/object/get/v2/get_range_forwarder.go
+++ b/pkg/services/object/get/v2/get_range_forwarder.go
@@ -132,6 +132,9 @@ func (f *getRangeRequestForwarder) readStream(ctx context.Context, rangeStream *
 		case *objectV2.SplitInfo:
 			si := objectSDK.NewSplitInfoFromV2(v)
 			return objectSDK.NewSplitInfoError(si)
+		case *objectV2.ECInfo:
+			ei := objectSDK.NewECInfoFromV2(v)
+			return objectSDK.NewECInfoError(ei)
 		}
 	}
 	return nil
diff --git a/pkg/services/object/get/v2/service.go b/pkg/services/object/get/v2/service.go
index 682128df67..edd19b441d 100644
--- a/pkg/services/object/get/v2/service.go
+++ b/pkg/services/object/get/v2/service.go
@@ -104,10 +104,13 @@ func (s *Service) GetRange(req *objectV2.GetRangeRequest, stream objectSvc.GetOb
 	err = s.svc.GetRange(stream.Context(), *p)
 
 	var splitErr *objectSDK.SplitInfoError
+	var ecErr *objectSDK.ECInfoError
 
 	switch {
 	case errors.As(err, &splitErr):
 		return stream.Send(splitInfoRangeResponse(splitErr.SplitInfo()))
+	case errors.As(err, &ecErr):
+		return stream.Send(ecInfoRangeResponse(ecErr.ECInfo()))
 	default:
 		return err
 	}
diff --git a/pkg/services/object/get/v2/util.go b/pkg/services/object/get/v2/util.go
index da64289852..610076c7a9 100644
--- a/pkg/services/object/get/v2/util.go
+++ b/pkg/services/object/get/v2/util.go
@@ -292,6 +292,17 @@ func splitInfoRangeResponse(info *objectSDK.SplitInfo) *objectV2.GetRangeRespons
 	return resp
 }
 
+func ecInfoRangeResponse(info *objectSDK.ECInfo) *objectV2.GetRangeResponse {
+	resp := new(objectV2.GetRangeResponse)
+
+	body := new(objectV2.GetRangeResponseBody)
+	resp.SetBody(body)
+
+	body.SetRangePart(info.ToV2())
+
+	return resp
+}
+
 func setSplitInfoHeadResponse(info *objectSDK.SplitInfo, resp *objectV2.HeadResponse) {
 	resp.GetBody().SetHeaderPart(info.ToV2())
 }