Allow S3 uploads without a timeout for exports (#21774)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
a8fa3f29e9
Коммит
61317883bf
@@ -1161,4 +1161,5 @@ type AppIface interface {
|
|||||||
VerifyUserEmail(userID, email string) *model.AppError
|
VerifyUserEmail(userID, email string) *model.AppError
|
||||||
ViewChannel(c request.CTX, view *model.ChannelView, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError)
|
ViewChannel(c request.CTX, view *model.ChannelView, userID string, currentSessionId string, collapsedThreadsSupported bool) (map[string]int64, *model.AppError)
|
||||||
WriteFile(fr io.Reader, path string) (int64, *model.AppError)
|
WriteFile(fr io.Reader, path string) (int64, *model.AppError)
|
||||||
|
WriteFileContext(ctx context.Context, fr io.Reader, path string) (int64, *model.AppError)
|
||||||
}
|
}
|
||||||
|
|||||||
28
app/file.go
28
app/file.go
@@ -161,6 +161,10 @@ func (a *App) MoveFile(oldPath, newPath string) *model.AppError {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) WriteFileContext(ctx context.Context, fr io.Reader, path string) (int64, *model.AppError) {
|
||||||
|
return a.Srv().writeFileContext(ctx, fr, path)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) WriteFile(fr io.Reader, path string) (int64, *model.AppError) {
|
func (a *App) WriteFile(fr io.Reader, path string) (int64, *model.AppError) {
|
||||||
return a.Srv().writeFile(fr, path)
|
return a.Srv().writeFile(fr, path)
|
||||||
}
|
}
|
||||||
@@ -173,6 +177,30 @@ func (s *Server) writeFile(fr io.Reader, path string) (int64, *model.AppError) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) writeFileContext(ctx context.Context, fr io.Reader, path string) (int64, *model.AppError) {
|
||||||
|
type ContextWriter interface {
|
||||||
|
WriteFileContext(context.Context, io.Reader, string) (int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
fileBackend = s.FileBackend()
|
||||||
|
written int64
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check if we can provide a custom context, otherwise just use the default method.
|
||||||
|
if cw, ok := fileBackend.(ContextWriter); ok {
|
||||||
|
written, err = cw.WriteFileContext(ctx, fr, path)
|
||||||
|
} else {
|
||||||
|
written, err = fileBackend.WriteFile(fr, path)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return written, model.NewAppError("WriteFile", "api.file.write_file.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return written, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) AppendFile(fr io.Reader, path string) (int64, *model.AppError) {
|
func (a *App) AppendFile(fr io.Reader, path string) (int64, *model.AppError) {
|
||||||
result, nErr := a.FileBackend().AppendFile(fr, path)
|
result, nErr := a.FileBackend().AppendFile(fr, path)
|
||||||
if nErr != nil {
|
if nErr != nil {
|
||||||
|
|||||||
@@ -18514,6 +18514,28 @@ func (a *OpenTracingAppLayer) WriteFile(fr io.Reader, path string) (int64, *mode
|
|||||||
return resultVar0, resultVar1
|
return resultVar0, resultVar1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *OpenTracingAppLayer) WriteFileContext(ctx context.Context, fr io.Reader, path string) (int64, *model.AppError) {
|
||||||
|
origCtx := a.ctx
|
||||||
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.WriteFileContext")
|
||||||
|
|
||||||
|
a.ctx = newCtx
|
||||||
|
a.app.Srv().Store().SetContext(newCtx)
|
||||||
|
defer func() {
|
||||||
|
a.app.Srv().Store().SetContext(origCtx)
|
||||||
|
a.ctx = origCtx
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer span.Finish()
|
||||||
|
resultVar0, resultVar1 := a.app.WriteFileContext(ctx, fr, path)
|
||||||
|
|
||||||
|
if resultVar1 != nil {
|
||||||
|
span.LogFields(spanlog.Error(resultVar1))
|
||||||
|
ext.Error.Set(span, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultVar0, resultVar1
|
||||||
|
}
|
||||||
|
|
||||||
func NewOpenTracingAppLayer(childApp app.AppIface, ctx context.Context) *OpenTracingAppLayer {
|
func NewOpenTracingAppLayer(childApp app.AppIface, ctx context.Context) *OpenTracingAppLayer {
|
||||||
newApp := OpenTracingAppLayer{
|
newApp := OpenTracingAppLayer{
|
||||||
app: childApp,
|
app: childApp,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
package export_process
|
package export_process
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ const jobName = "ExportProcess"
|
|||||||
type AppIface interface {
|
type AppIface interface {
|
||||||
configservice.ConfigService
|
configservice.ConfigService
|
||||||
WriteFile(fr io.Reader, path string) (int64, *model.AppError)
|
WriteFile(fr io.Reader, path string) (int64, *model.AppError)
|
||||||
|
WriteFileContext(ctx context.Context, fr io.Reader, path string) (int64, *model.AppError)
|
||||||
BulkExport(ctx request.CTX, writer io.Writer, outPath string, opts model.BulkExportOpts) *model.AppError
|
BulkExport(ctx request.CTX, writer io.Writer, outPath string, opts model.BulkExportOpts) *model.AppError
|
||||||
Log() *mlog.Logger
|
Log() *mlog.Logger
|
||||||
}
|
}
|
||||||
@@ -45,7 +47,8 @@ func MakeWorker(jobServer *jobs.JobServer, app AppIface) model.Worker {
|
|||||||
errCh := make(chan *model.AppError, 1)
|
errCh := make(chan *model.AppError, 1)
|
||||||
go func() {
|
go func() {
|
||||||
defer close(errCh)
|
defer close(errCh)
|
||||||
_, appErr := app.WriteFile(rd, filepath.Join(outPath, exportFilename))
|
// Try to write without a timeout
|
||||||
|
_, appErr := app.WriteFileContext(context.Background(), rd, filepath.Join(outPath, exportFilename))
|
||||||
errCh <- appErr
|
errCh <- appErr
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ package filestore
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -121,6 +124,91 @@ func (s *FileBackendTestSuite) TestReadWriteFile() {
|
|||||||
s.EqualValues(readString, "test")
|
s.EqualValues(readString, "test")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *FileBackendTestSuite) TestReadWriteFileContext() {
|
||||||
|
type ContextWriter interface {
|
||||||
|
WriteFileContext(context.Context, io.Reader, string) (int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := "test"
|
||||||
|
|
||||||
|
s.T().Run("no deadline", func(t *testing.T) {
|
||||||
|
var (
|
||||||
|
written int64
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
path := "tests/" + randomString()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
if cw, ok := s.backend.(ContextWriter); ok {
|
||||||
|
written, err = cw.WriteFileContext(ctx, strings.NewReader(data), path)
|
||||||
|
} else {
|
||||||
|
written, err = s.backend.WriteFile(strings.NewReader(data), path)
|
||||||
|
}
|
||||||
|
s.NoError(err)
|
||||||
|
s.EqualValues(len(data), written, "expected given number of bytes to have been written")
|
||||||
|
defer s.backend.RemoveFile(path)
|
||||||
|
|
||||||
|
read, err := s.backend.ReadFile(path)
|
||||||
|
s.NoError(err)
|
||||||
|
|
||||||
|
readString := string(read)
|
||||||
|
s.Equal(readString, data)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.T().Run("long deadline", func(t *testing.T) {
|
||||||
|
var (
|
||||||
|
written int64
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
path := "tests/" + randomString()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if cw, ok := s.backend.(ContextWriter); ok {
|
||||||
|
written, err = cw.WriteFileContext(ctx, strings.NewReader(data), path)
|
||||||
|
} else {
|
||||||
|
written, err = s.backend.WriteFile(strings.NewReader(data), path)
|
||||||
|
}
|
||||||
|
s.NoError(err)
|
||||||
|
s.EqualValues(len(data), written, "expected given number of bytes to have been written")
|
||||||
|
defer s.backend.RemoveFile(path)
|
||||||
|
|
||||||
|
read, err := s.backend.ReadFile(path)
|
||||||
|
s.NoError(err)
|
||||||
|
|
||||||
|
readString := string(read)
|
||||||
|
s.Equal(readString, data)
|
||||||
|
})
|
||||||
|
|
||||||
|
s.T().Run("missed deadline", func(t *testing.T) {
|
||||||
|
var (
|
||||||
|
written int64
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
path := "tests/" + randomString()
|
||||||
|
|
||||||
|
r, w := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
// close the writer after a short time
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
w.Close()
|
||||||
|
}()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
if cw, ok := s.backend.(ContextWriter); ok {
|
||||||
|
written, err = cw.WriteFileContext(ctx, r, path)
|
||||||
|
} else {
|
||||||
|
// this test works only with a context writer
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Error(err)
|
||||||
|
s.Zero(written)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (s *FileBackendTestSuite) TestReadWriteFileImage() {
|
func (s *FileBackendTestSuite) TestReadWriteFileImage() {
|
||||||
b := []byte("testimage")
|
b := []byte("testimage")
|
||||||
path := "tests/" + randomString() + ".png"
|
path := "tests/" + randomString() + ".png"
|
||||||
|
|||||||
@@ -369,6 +369,13 @@ func (b *S3FileBackend) MoveFile(oldPath, newPath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *S3FileBackend) WriteFile(fr io.Reader, path string) (int64, error) {
|
func (b *S3FileBackend) WriteFile(fr io.Reader, path string) (int64, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
return b.WriteFileContext(ctx, fr, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *S3FileBackend) WriteFileContext(ctx context.Context, fr io.Reader, path string) (int64, error) {
|
||||||
var contentType string
|
var contentType string
|
||||||
path = filepath.Join(b.pathPrefix, path)
|
path = filepath.Join(b.pathPrefix, path)
|
||||||
if ext := filepath.Ext(path); isFileExtImage(ext) {
|
if ext := filepath.Ext(path); isFileExtImage(ext) {
|
||||||
@@ -377,22 +384,26 @@ func (b *S3FileBackend) WriteFile(fr io.Reader, path string) (int64, error) {
|
|||||||
contentType = "binary/octet-stream"
|
contentType = "binary/octet-stream"
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), b.timeout)
|
|
||||||
defer cancel()
|
|
||||||
options := s3PutOptions(b.encrypt, contentType)
|
options := s3PutOptions(b.encrypt, contentType)
|
||||||
|
|
||||||
objSize := -1
|
objSize := int64(-1)
|
||||||
isCloud := os.Getenv("MM_CLOUD_FILESTORE_BIFROST") != ""
|
isCloud := os.Getenv("MM_CLOUD_FILESTORE_BIFROST") != ""
|
||||||
if isCloud {
|
if isCloud {
|
||||||
options.DisableContentSha256 = true
|
options.DisableContentSha256 = true
|
||||||
}
|
} else {
|
||||||
// We pass an object size only in situations where bifrost is not
|
// We pass an object size only in situations where bifrost is not
|
||||||
// used. Bifrost needs to run in HTTPS, which is not yet deployed.
|
// used. Bifrost needs to run in HTTPS, which is not yet deployed.
|
||||||
if buf, ok := fr.(*bytes.Buffer); ok && !isCloud {
|
switch t := fr.(type) {
|
||||||
objSize = buf.Len()
|
case *bytes.Buffer:
|
||||||
|
objSize = int64(t.Len())
|
||||||
|
case *os.File:
|
||||||
|
if s, err := t.Stat(); err == nil {
|
||||||
|
objSize = s.Size()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := b.client.PutObject(ctx, b.bucket, path, fr, int64(objSize), options)
|
info, err := b.client.PutObject(ctx, b.bucket, path, fr, objSize, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return info.Size, errors.Wrapf(err, "unable write the data in the file %s", path)
|
return info.Size, errors.Wrapf(err, "unable write the data in the file %s", path)
|
||||||
}
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user