Skip to content

Commit c413336

Browse files
committed
core: introduce object chunk and chunk manifest; unit test
* part one Signed-off-by: Alex Aizman <alex.aizman@gmail.com>
1 parent 8006205 commit c413336

File tree

7 files changed

+574
-18
lines changed

7 files changed

+574
-18
lines changed

ais/target.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ func (t *target) Run() error {
395395
// register object type and workfile type
396396
fs.CSM.Reg(fs.ObjectType, &fs.ObjectContentResolver{})
397397
fs.CSM.Reg(fs.WorkfileType, &fs.WorkfileContentResolver{})
398+
fs.CSM.Reg(fs.ObjChunkType, &fs.ObjChunkContentResolver{})
398399

399400
// Init meta-owners and load local instances
400401
if prev := t.owner.bmd.init(); prev {

core/chunk_test.go

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
// Package core_test provides tests for cluster package
2+
/*
3+
* Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
4+
*/
5+
package core_test
6+
7+
import (
8+
"os"
9+
10+
"github.com/NVIDIA/aistore/api/apc"
11+
"github.com/NVIDIA/aistore/cmn"
12+
"github.com/NVIDIA/aistore/cmn/cos"
13+
"github.com/NVIDIA/aistore/core"
14+
"github.com/NVIDIA/aistore/core/meta"
15+
"github.com/NVIDIA/aistore/core/mock"
16+
"github.com/NVIDIA/aistore/fs"
17+
"github.com/NVIDIA/aistore/tools/trand"
18+
19+
. "github.com/onsi/ginkgo/v2"
20+
. "github.com/onsi/gomega"
21+
)
22+
23+
// on-disk xattr name for chunks
24+
const (
25+
xattrChunk = "user.ais.chunk"
26+
)
27+
28+
var _ = Describe("Chunk Manifest Xattrs", func() {
29+
const (
30+
tmpDir = "/tmp/chunk_xattr_test"
31+
xattrMpath = tmpDir + "/xattr"
32+
33+
bucketLocal = "CHUNK_TEST_Local"
34+
)
35+
36+
localBck := cmn.Bck{Name: bucketLocal, Provider: apc.AIS, Ns: cmn.NsGlobal}
37+
38+
fs.CSM.Reg(fs.ObjectType, &fs.ObjectContentResolver{}, true)
39+
fs.CSM.Reg(fs.ObjChunkType, &fs.ObjChunkContentResolver{}, true)
40+
41+
var (
42+
mix = fs.Mountpath{Path: xattrMpath}
43+
bmdMock = mock.NewBaseBownerMock(
44+
meta.NewBck(
45+
bucketLocal, apc.AIS, cmn.NsGlobal,
46+
&cmn.Bprops{Cksum: cmn.CksumConf{Type: cos.ChecksumOneXxh}, BID: 201},
47+
),
48+
)
49+
)
50+
51+
BeforeEach(func() {
52+
_ = cos.CreateDir(xattrMpath)
53+
_, _ = fs.Add(xattrMpath, "daeID")
54+
_ = mock.NewTarget(bmdMock)
55+
})
56+
57+
AfterEach(func() {
58+
_, _ = fs.Remove(xattrMpath)
59+
_ = os.RemoveAll(tmpDir)
60+
})
61+
62+
Describe("chunk manifest", func() {
63+
var (
64+
testFileSize = int64(1024 * 1024) // 1MB
65+
testObjectName = "chunked/test-obj.bin"
66+
localFQN = mix.MakePathFQN(&localBck, fs.ObjectType, testObjectName)
67+
)
68+
69+
createChunkManifest := func(totalSize int64, numChunks uint16, chunkSizes []int64) *core.Ufest {
70+
manifest := &core.Ufest{
71+
Size: totalSize,
72+
Num: numChunks,
73+
CksumTyp: cos.ChecksumOneXxh,
74+
Chunks: make([]core.Uchunk, numChunks),
75+
}
76+
77+
for i := range numChunks {
78+
manifest.Chunks[i] = core.Uchunk{
79+
Siz: chunkSizes[i],
80+
CksumVal: trand.String(16), // mock checksum
81+
}
82+
}
83+
return manifest
84+
}
85+
86+
Describe("Store and Load", func() {
87+
It("should store and load chunk manifest correctly", func() {
88+
// Create test file (chunk #1)
89+
createTestFile(localFQN, int(testFileSize))
90+
lom := NewBasicLom(localFQN)
91+
92+
// Create chunk manifest for 3 chunks - ensure sizes sum to testFileSize
93+
chunkSizes := []int64{400000, 400000, 248576} // total = 1048576 (1MB)
94+
manifest := createChunkManifest(testFileSize, 3, chunkSizes)
95+
96+
// Store manifest
97+
err := manifest.Store(lom)
98+
Expect(err).NotTo(HaveOccurred())
99+
100+
// Verify xattr was written
101+
b, err := fs.GetXattr(localFQN, xattrChunk)
102+
Expect(b).ToNot(BeEmpty())
103+
Expect(err).NotTo(HaveOccurred())
104+
105+
// Load manifest from disk
106+
loadedManifest := &core.Ufest{}
107+
err = loadedManifest.Load(lom)
108+
Expect(err).NotTo(HaveOccurred())
109+
110+
// Verify manifest contents
111+
Expect(loadedManifest.Size).To(Equal(testFileSize))
112+
Expect(loadedManifest.Num).To(Equal(uint16(3)))
113+
Expect(loadedManifest.CksumTyp).To(Equal(cos.ChecksumOneXxh))
114+
Expect(loadedManifest.Chunks).To(HaveLen(3))
115+
116+
for i := range 3 {
117+
Expect(loadedManifest.Chunks[i].Siz).To(Equal(chunkSizes[i]))
118+
Expect(loadedManifest.Chunks[i].CksumVal).To(Equal(manifest.Chunks[i].CksumVal))
119+
}
120+
})
121+
122+
It("should handle single chunk manifest", func() {
123+
createTestFile(localFQN, int(testFileSize))
124+
lom := NewBasicLom(localFQN)
125+
126+
// Single chunk manifest
127+
chunkSizes := []int64{testFileSize}
128+
manifest := createChunkManifest(testFileSize, 1, chunkSizes)
129+
130+
err := manifest.Store(lom)
131+
Expect(err).NotTo(HaveOccurred())
132+
133+
loadedManifest := &core.Ufest{}
134+
err = loadedManifest.Load(lom)
135+
Expect(err).NotTo(HaveOccurred())
136+
137+
Expect(loadedManifest.Num).To(Equal(uint16(1)))
138+
Expect(loadedManifest.Chunks[0].Siz).To(Equal(testFileSize))
139+
})
140+
141+
It("should handle many small chunks", func() {
142+
createTestFile(localFQN, int(testFileSize))
143+
lom := NewBasicLom(localFQN)
144+
145+
// 100 chunks of ~10KB each
146+
numChunks := uint16(100)
147+
chunkSize := testFileSize / int64(numChunks)
148+
chunkSizes := make([]int64, numChunks)
149+
for i := range numChunks {
150+
chunkSizes[i] = chunkSize
151+
}
152+
// Adjust last chunk for remainder
153+
chunkSizes[numChunks-1] += testFileSize % int64(numChunks)
154+
155+
manifest := createChunkManifest(testFileSize, numChunks, chunkSizes)
156+
157+
err := manifest.Store(lom)
158+
Expect(err).NotTo(HaveOccurred())
159+
160+
loadedManifest := &core.Ufest{}
161+
err = loadedManifest.Load(lom)
162+
Expect(err).NotTo(HaveOccurred())
163+
164+
Expect(loadedManifest.Num).To(Equal(numChunks))
165+
Expect(loadedManifest.Chunks).To(HaveLen(int(numChunks)))
166+
167+
var totalSize int64
168+
for _, chunk := range loadedManifest.Chunks {
169+
totalSize += chunk.Siz
170+
}
171+
Expect(totalSize).To(Equal(testFileSize))
172+
})
173+
})
174+
175+
Describe("validation", func() {
176+
It("should fail when num doesn't match chunks length", func() {
177+
createTestFile(localFQN, int(testFileSize))
178+
lom := NewBasicLom(localFQN)
179+
180+
// Create invalid manifest - num says 3 but only 2 chunks
181+
manifest := &core.Ufest{
182+
Size: testFileSize,
183+
Num: 3,
184+
CksumTyp: cos.ChecksumOneXxh,
185+
Chunks: []core.Uchunk{
186+
{Siz: 500000, CksumVal: "abc123"},
187+
{Siz: 524000, CksumVal: "def456"},
188+
},
189+
}
190+
191+
err := manifest.Store(lom)
192+
Expect(err).To(HaveOccurred())
193+
Expect(err.Error()).To(ContainSubstring("invalid manifest num=3, len=2"))
194+
})
195+
196+
It("should fail when num is zero", func() {
197+
createTestFile(localFQN, int(testFileSize))
198+
lom := NewBasicLom(localFQN)
199+
200+
manifest := &core.Ufest{
201+
Size: testFileSize,
202+
Num: 0,
203+
CksumTyp: cos.ChecksumOneXxh,
204+
Chunks: []core.Uchunk{},
205+
}
206+
207+
err := manifest.Store(lom)
208+
Expect(err).To(HaveOccurred())
209+
Expect(err.Error()).To(ContainSubstring("invalid manifest num=0"))
210+
})
211+
212+
It("should fail when manifest is too large for xattr", func() {
213+
createTestFile(localFQN, int(testFileSize))
214+
lom := NewBasicLom(localFQN)
215+
216+
// Create a manifest that will exceed xattr size limits
217+
// Use many chunks with long checksum values
218+
numChunks := uint16(1000)
219+
chunkSizes := make([]int64, numChunks)
220+
for i := range numChunks {
221+
chunkSizes[i] = 1024
222+
}
223+
224+
manifest := createChunkManifest(int64(numChunks)*1024, numChunks, chunkSizes)
225+
// Make checksum values very long to exceed xattr limits
226+
for i := range manifest.Chunks {
227+
manifest.Chunks[i].CksumVal = trand.String(1000)
228+
}
229+
230+
err := manifest.Store(lom)
231+
Expect(err).To(HaveOccurred())
232+
Expect(err.Error()).To(ContainSubstring("manifest too large"))
233+
})
234+
})
235+
236+
Describe("serialization errors", func() {
237+
It("should fail when loading non-existent manifest", func() {
238+
createTestFile(localFQN, int(testFileSize))
239+
lom := NewBasicLom(localFQN)
240+
241+
manifest := &core.Ufest{}
242+
err := manifest.Load(lom)
243+
Expect(err).To(HaveOccurred())
244+
Expect(err.Error()).To(ContainSubstring("chunk-manifest"))
245+
})
246+
247+
It("should fail when metadata version is invalid", func() {
248+
createTestFile(localFQN, int(testFileSize))
249+
lom := NewBasicLom(localFQN)
250+
251+
// Store valid manifest first
252+
chunkSizes := []int64{testFileSize}
253+
manifest := createChunkManifest(testFileSize, 1, chunkSizes)
254+
err := manifest.Store(lom)
255+
Expect(err).NotTo(HaveOccurred())
256+
257+
// Corrupt the version byte
258+
b, err := fs.GetXattr(localFQN, xattrChunk)
259+
Expect(err).NotTo(HaveOccurred())
260+
b[0] = 99 // invalid version
261+
Expect(fs.SetXattr(localFQN, xattrChunk, b)).NotTo(HaveOccurred())
262+
263+
// Loading should fail
264+
loadedManifest := &core.Ufest{}
265+
err = loadedManifest.Load(lom)
266+
Expect(err).To(HaveOccurred())
267+
Expect(err.Error()).To(ContainSubstring("unsupported chunk-manifest meta-version 99"))
268+
})
269+
270+
It("should fail when checksum verification fails", func() {
271+
createTestFile(localFQN, int(testFileSize))
272+
lom := NewBasicLom(localFQN)
273+
274+
// Store valid manifest
275+
chunkSizes := []int64{testFileSize}
276+
manifest := createChunkManifest(testFileSize, 1, chunkSizes)
277+
err := manifest.Store(lom)
278+
Expect(err).NotTo(HaveOccurred())
279+
280+
b, err := fs.GetXattr(localFQN, xattrChunk)
281+
Expect(err).NotTo(HaveOccurred())
282+
283+
// corrupting data that's already been parsed but is still part of the checksummed payload.
284+
corruptIdx := len(b) - 10
285+
b[corruptIdx]++
286+
287+
Expect(fs.SetXattr(localFQN, xattrChunk, b)).NotTo(HaveOccurred())
288+
289+
// Loading should fail due to checksum mismatch
290+
loadedManifest := &core.Ufest{}
291+
err = loadedManifest.Load(lom)
292+
Expect(err).To(HaveOccurred())
293+
Expect(err.Error()).To(ContainSubstring("BAD META CHECKSUM"))
294+
})
295+
296+
It("should fail when xattr data is truncated", func() {
297+
createTestFile(localFQN, int(testFileSize))
298+
lom := NewBasicLom(localFQN)
299+
300+
// Store valid manifest
301+
chunkSizes := []int64{testFileSize}
302+
manifest := createChunkManifest(testFileSize, 1, chunkSizes)
303+
err := manifest.Store(lom)
304+
Expect(err).NotTo(HaveOccurred())
305+
306+
// Truncate the xattr data
307+
b, err := fs.GetXattr(localFQN, xattrChunk)
308+
Expect(err).NotTo(HaveOccurred())
309+
truncated := b[:len(b)/2] // cut in half
310+
Expect(fs.SetXattr(localFQN, xattrChunk, truncated)).NotTo(HaveOccurred())
311+
312+
// Loading should fail
313+
loadedManifest := &core.Ufest{}
314+
err = loadedManifest.Load(lom)
315+
Expect(err).To(HaveOccurred())
316+
})
317+
})
318+
319+
Describe("packing/unpacking edge cases", func() {
320+
It("should handle empty checksum values", func() {
321+
createTestFile(localFQN, int(testFileSize))
322+
lom := NewBasicLom(localFQN)
323+
324+
manifest := &core.Ufest{
325+
Size: testFileSize,
326+
Num: 2,
327+
CksumTyp: cos.ChecksumOneXxh,
328+
Chunks: []core.Uchunk{
329+
{Siz: 500000, CksumVal: ""}, // empty checksum
330+
{Siz: 548576, CksumVal: "valid_checksum"}, // total = 1048576
331+
},
332+
}
333+
334+
err := manifest.Store(lom)
335+
Expect(err).NotTo(HaveOccurred())
336+
337+
loadedManifest := &core.Ufest{}
338+
err = loadedManifest.Load(lom)
339+
Expect(err).NotTo(HaveOccurred())
340+
341+
Expect(loadedManifest.Chunks[0].CksumVal).To(Equal(""))
342+
Expect(loadedManifest.Chunks[1].CksumVal).To(Equal("valid_checksum"))
343+
})
344+
345+
It("should handle zero-sized chunks", func() {
346+
createTestFile(localFQN, int(testFileSize))
347+
lom := NewBasicLom(localFQN)
348+
349+
manifest := &core.Ufest{
350+
Size: testFileSize,
351+
Num: 3,
352+
CksumTyp: cos.ChecksumOneXxh,
353+
Chunks: []core.Uchunk{
354+
{Siz: 0, CksumVal: "empty"}, // zero size
355+
{Siz: testFileSize, CksumVal: "full"},
356+
{Siz: 0, CksumVal: "empty2"}, // another zero - total = 1048576
357+
},
358+
}
359+
360+
err := manifest.Store(lom)
361+
Expect(err).NotTo(HaveOccurred())
362+
363+
loadedManifest := &core.Ufest{}
364+
err = loadedManifest.Load(lom)
365+
Expect(err).NotTo(HaveOccurred())
366+
367+
Expect(loadedManifest.Chunks[0].Siz).To(Equal(int64(0)))
368+
Expect(loadedManifest.Chunks[1].Siz).To(Equal(testFileSize))
369+
Expect(loadedManifest.Chunks[2].Siz).To(Equal(int64(0)))
370+
})
371+
})
372+
})
373+
})

0 commit comments

Comments
 (0)