Skip to content

Commit b357b0e

Browse files
author
Jason Yellick
committed
FAB-16158 Create streaming package retriever
This CR re-implements the package parsing in a streaming fashion, rather than relying on pulling the whole code package into RAM as the previous implementation did. Note, the old implementation still exists, and is still used by the non-streaming code, notably the lifecycle SCC. We could probably transition that code onto this interface. Change-Id: Ib8216c57570b2f662359d4818a6614d53c4a2347 Signed-off-by: Jason Yellick <jyellick@us.ibm.com>
1 parent a3efbb7 commit b357b0e

File tree

4 files changed

+248
-0
lines changed

4 files changed

+248
-0
lines changed

core/chaincode/persistence/chaincode_package.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@ import (
1313
"encoding/json"
1414
"io"
1515
"io/ioutil"
16+
"os"
17+
"path/filepath"
1618
"regexp"
1719

20+
"github.com/hyperledger/fabric/core/chaincode/persistence/intf" // yuck
21+
1822
"github.com/pkg/errors"
1923
)
2024

@@ -41,6 +45,105 @@ const (
4145
ChaincodePackageMetadataFile = "Chaincode-Package-Metadata.json"
4246
)
4347

48+
type ChaincodePackageLocator struct {
49+
ChaincodeDir string
50+
}
51+
52+
func (cpl *ChaincodePackageLocator) ChaincodePackageStreamer(packageID persistence.PackageID) *ChaincodePackageStreamer {
53+
return &ChaincodePackageStreamer{
54+
PackagePath: filepath.Join(cpl.ChaincodeDir, packageID.String()+".bin"),
55+
}
56+
}
57+
58+
type ChaincodePackageStreamer struct {
59+
PackagePath string
60+
}
61+
62+
func (cps *ChaincodePackageStreamer) Metadata() (*ChaincodePackageMetadata, error) {
63+
tarFileStream, err := cps.File(PreferredChaincodePackageMetadataFile)
64+
if err != nil {
65+
return nil, errors.WithMessage(err, "could not get metadata file")
66+
}
67+
68+
defer tarFileStream.Close()
69+
70+
metadata := &ChaincodePackageMetadata{}
71+
err = json.NewDecoder(tarFileStream).Decode(metadata)
72+
if err != nil {
73+
return nil, errors.WithMessage(err, "could not parse metadata file")
74+
}
75+
76+
return metadata, nil
77+
}
78+
79+
func (cps *ChaincodePackageStreamer) Code() (*TarFileStream, error) {
80+
tarFileStream, err := cps.File(CodePackageFile)
81+
if err != nil {
82+
return nil, errors.WithMessage(err, "could not get code package")
83+
}
84+
85+
return tarFileStream, nil
86+
}
87+
88+
func (cps *ChaincodePackageStreamer) File(name string) (tarFileStream *TarFileStream, err error) {
89+
file, err := os.Open(cps.PackagePath)
90+
if err != nil {
91+
return nil, errors.WithMessagef(err, "could not open chaincode package at '%s'", cps.PackagePath)
92+
}
93+
94+
defer func() {
95+
if err != nil {
96+
file.Close()
97+
}
98+
}()
99+
100+
gzReader, err := gzip.NewReader(file)
101+
if err != nil {
102+
return nil, errors.Wrapf(err, "error reading as gzip stream")
103+
}
104+
105+
tarReader := tar.NewReader(gzReader)
106+
107+
for {
108+
header, err := tarReader.Next()
109+
if err == io.EOF {
110+
break
111+
}
112+
113+
if err != nil {
114+
return nil, errors.Wrapf(err, "error inspecting next tar header")
115+
}
116+
117+
if header.Name != name {
118+
continue
119+
}
120+
121+
if header.Typeflag != tar.TypeReg {
122+
return nil, errors.Errorf("tar entry %s is not a regular file, type %v", header.Name, header.Typeflag)
123+
}
124+
125+
return &TarFileStream{
126+
TarFile: tarReader,
127+
FileStream: file,
128+
}, nil
129+
}
130+
131+
return nil, errors.Errorf("did not find file '%s' in package", name)
132+
}
133+
134+
type TarFileStream struct {
135+
TarFile io.Reader
136+
FileStream io.Closer
137+
}
138+
139+
func (tfs *TarFileStream) Read(p []byte) (int, error) {
140+
return tfs.TarFile.Read(p)
141+
}
142+
143+
func (tfs *TarFileStream) Close() error {
144+
return tfs.FileStream.Close()
145+
}
146+
44147
// ChaincodePackage represents the un-tar-ed format of the chaincode package.
45148
type ChaincodePackage struct {
46149
Metadata *ChaincodePackageMetadata

core/chaincode/persistence/chaincode_package_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,147 @@ var _ = Describe("ChaincodePackageParser", func() {
162162
})
163163
})
164164
})
165+
166+
var _ = Describe("ChaincodePackageLocator", func() {
167+
var (
168+
locator *persistence.ChaincodePackageLocator
169+
)
170+
171+
BeforeEach(func() {
172+
locator = &persistence.ChaincodePackageLocator{
173+
ChaincodeDir: "/fake-dir",
174+
}
175+
})
176+
177+
Describe("ChaincodePackageStreamer", func() {
178+
It("creates a ChaincodePackageStreamer for the given packageID", func() {
179+
streamer := locator.ChaincodePackageStreamer("test-package")
180+
Expect(streamer).To(Equal(&persistence.ChaincodePackageStreamer{
181+
PackagePath: "/fake-dir/test-package.bin",
182+
}))
183+
})
184+
})
185+
})
186+
187+
var _ = Describe("ChaincodePackageStreamer", func() {
188+
var (
189+
streamer *persistence.ChaincodePackageStreamer
190+
)
191+
192+
BeforeEach(func() {
193+
streamer = &persistence.ChaincodePackageStreamer{
194+
PackagePath: "testdata/good-package.tar.gz",
195+
}
196+
})
197+
198+
Describe("Metadata", func() {
199+
It("reads the metadata from the package", func() {
200+
md, err := streamer.Metadata()
201+
Expect(err).NotTo(HaveOccurred())
202+
Expect(md).To(Equal(&persistence.ChaincodePackageMetadata{
203+
Type: "Fake-Type",
204+
Path: "Fake-Path",
205+
Label: "Real-Label",
206+
}))
207+
})
208+
209+
Context("when the metadata file cannot be found", func() {
210+
BeforeEach(func() {
211+
streamer.PackagePath = "testdata/missing-metadata.tar.gz"
212+
})
213+
214+
It("wraps and returns the error", func() {
215+
_, err := streamer.Metadata()
216+
Expect(err).To(MatchError("could not get metadata file: did not find file 'metadata.json' in package"))
217+
})
218+
})
219+
220+
Context("when the metadata file cannot be parsed", func() {
221+
BeforeEach(func() {
222+
streamer.PackagePath = "testdata/bad-metadata.tar.gz"
223+
})
224+
225+
It("wraps and returns the error", func() {
226+
_, err := streamer.Metadata()
227+
Expect(err).To(MatchError("could not parse metadata file: invalid character '\\n' in string literal"))
228+
})
229+
})
230+
})
231+
232+
Describe("Code", func() {
233+
It("reads a file from the package", func() {
234+
code, err := streamer.Code()
235+
Expect(err).NotTo(HaveOccurred())
236+
codeBytes, err := ioutil.ReadAll(code)
237+
code.Close()
238+
Expect(err).NotTo(HaveOccurred())
239+
Expect(codeBytes).To(Equal([]byte("package")))
240+
})
241+
242+
Context("when the file cannot be found because the code is not a regular file", func() {
243+
BeforeEach(func() {
244+
streamer.PackagePath = "testdata/missing-codepackage.tar.gz"
245+
})
246+
247+
It("wraps and returns the error", func() {
248+
_, err := streamer.Code()
249+
Expect(err).To(MatchError("could not get code package: did not find file 'code.tar.gz' in package"))
250+
})
251+
})
252+
})
253+
254+
Describe("File", func() {
255+
It("reads a file from the package", func() {
256+
code, err := streamer.File("code.tar.gz")
257+
Expect(err).NotTo(HaveOccurred())
258+
codeBytes, err := ioutil.ReadAll(code)
259+
code.Close()
260+
Expect(err).NotTo(HaveOccurred())
261+
Expect(codeBytes).To(Equal([]byte("package")))
262+
})
263+
264+
Context("when the file is not a regular file", func() {
265+
BeforeEach(func() {
266+
streamer.PackagePath = "testdata/non-regular-file.tar.gz"
267+
})
268+
269+
It("wraps and returns the error", func() {
270+
_, err := streamer.File("code.tar.gz")
271+
Expect(err).To(MatchError("tar entry code.tar.gz is not a regular file, type 50"))
272+
})
273+
})
274+
275+
Context("when the code cannot be found because the archive is corrupt", func() {
276+
BeforeEach(func() {
277+
streamer.PackagePath = "testdata/bad-archive.tar.gz"
278+
})
279+
280+
It("wraps and returns the error", func() {
281+
_, err := streamer.File("code.tar.gz")
282+
Expect(err).To(MatchError("could not open chaincode package at 'testdata/bad-archive.tar.gz': open testdata/bad-archive.tar.gz: no such file or directory"))
283+
})
284+
})
285+
286+
Context("when the code cannot be found because the header is corrupt", func() {
287+
BeforeEach(func() {
288+
streamer.PackagePath = "testdata/corrupted-header.tar.gz"
289+
})
290+
291+
It("wraps and returns the error", func() {
292+
_, err := streamer.File("code.tar.gz")
293+
Expect(err).To(MatchError("error inspecting next tar header: flate: corrupt input before offset 86"))
294+
})
295+
})
296+
297+
Context("when the code cannot be found because the gzip is corrupt", func() {
298+
BeforeEach(func() {
299+
streamer.PackagePath = "testdata/corrupted-gzip.tar.gz"
300+
})
301+
302+
It("wraps and returns the error", func() {
303+
_, err := streamer.File("code.tar.gz")
304+
Expect(err).To(MatchError("error reading as gzip stream: unexpected EOF"))
305+
})
306+
})
307+
})
308+
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
notagzip
Binary file not shown.

0 commit comments

Comments
 (0)