mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-15 20:02:15 +01:00
feat(libreoffice): retry on core dumped (#930)
This commit is contained in:
@@ -28,6 +28,11 @@ var (
|
|||||||
// ErrMalformedPageRanges happens if the page ranges option cannot be
|
// ErrMalformedPageRanges happens if the page ranges option cannot be
|
||||||
// interpreted by LibreOffice.
|
// interpreted by LibreOffice.
|
||||||
ErrMalformedPageRanges = errors.New("page ranges are malformed")
|
ErrMalformedPageRanges = errors.New("page ranges are malformed")
|
||||||
|
|
||||||
|
// ErrCoreDumped happens randomly; sometime a conversion will work as
|
||||||
|
// expected, and some other time the same conversion will fail.
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/639.
|
||||||
|
ErrCoreDumped = errors.New("core dumped")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Api is a module which provides a [Uno] to interact with LibreOffice.
|
// Api is a module which provides a [Uno] to interact with LibreOffice.
|
||||||
@@ -365,9 +370,20 @@ func (a *Api) LibreOffice() (Uno, error) {
|
|||||||
|
|
||||||
// Pdf converts a document to PDF.
|
// Pdf converts a document to PDF.
|
||||||
func (a *Api) Pdf(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
|
func (a *Api) Pdf(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
|
||||||
return a.supervisor.Run(ctx, logger, func() error {
|
err := a.supervisor.Run(ctx, logger, func() error {
|
||||||
return a.libreOffice.pdf(ctx, logger, inputPath, outputPath, options)
|
return a.libreOffice.pdf(ctx, logger, inputPath, outputPath, options)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/639.
|
||||||
|
if errors.Is(err, ErrCoreDumped) {
|
||||||
|
return a.Pdf(ctx, logger, inputPath, outputPath, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("supervisor run task: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extensions returns the file extensions available for conversions.
|
// Extensions returns the file extensions available for conversions.
|
||||||
|
|||||||
@@ -433,6 +433,13 @@ func TestApi_Pdf(t *testing.T) {
|
|||||||
}},
|
}},
|
||||||
expectError: true,
|
expectError: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "ErrCoreDumped",
|
||||||
|
libreOffice: &libreOfficeMock{pdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error {
|
||||||
|
return ErrCoreDumped
|
||||||
|
}},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.scenario, func(t *testing.T) {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
a := new(Api)
|
a := new(Api)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@@ -348,6 +349,12 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
|
|||||||
return ErrMalformedPageRanges
|
return ErrMalformedPageRanges
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We may want to retry in case of a core dumped event.
|
||||||
|
// See https://github.com/gotenberg/gotenberg/issues/639.
|
||||||
|
if strings.Contains(err.Error(), "core dumped") {
|
||||||
|
return ErrCoreDumped
|
||||||
|
}
|
||||||
|
|
||||||
// Possible errors:
|
// Possible errors:
|
||||||
// 1. LibreOffice failed for some reason.
|
// 1. LibreOffice failed for some reason.
|
||||||
// 2. Context done.
|
// 2. Context done.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
@@ -33,12 +34,21 @@ func (provider *ProviderMock) LibreOffice() (Uno, error) {
|
|||||||
|
|
||||||
// libreOfficeMock is a mock for the [libreOffice] interface.
|
// libreOfficeMock is a mock for the [libreOffice] interface.
|
||||||
type libreOfficeMock struct {
|
type libreOfficeMock struct {
|
||||||
|
errCoreDumpedCount int
|
||||||
|
|
||||||
gotenberg.ProcessMock
|
gotenberg.ProcessMock
|
||||||
pdfMock func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error
|
pdfMock func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *libreOfficeMock) pdf(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
|
func (b *libreOfficeMock) pdf(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
|
||||||
return b.pdfMock(ctx, logger, inputPath, outputPath, options)
|
err := b.pdfMock(ctx, logger, inputPath, outputPath, options)
|
||||||
|
if errors.Is(err, ErrCoreDumped) {
|
||||||
|
b.errCoreDumpedCount += 1
|
||||||
|
}
|
||||||
|
if b.errCoreDumpedCount > 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
|
|||||||
@@ -42,14 +42,53 @@ func TestProviderMock(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLibreOfficeMock(t *testing.T) {
|
func TestLibreOfficeMock(t *testing.T) {
|
||||||
mock := &libreOfficeMock{
|
for _, tc := range []struct {
|
||||||
pdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error {
|
scenario string
|
||||||
return nil
|
mock *libreOfficeMock
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "success",
|
||||||
|
mock: &libreOfficeMock{
|
||||||
|
pdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
},
|
},
|
||||||
}
|
{
|
||||||
|
scenario: "ErrCoreDumped (first call)",
|
||||||
|
mock: &libreOfficeMock{
|
||||||
|
pdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error {
|
||||||
|
return ErrCoreDumped
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "ErrCoreDumped (second call)",
|
||||||
|
mock: func() *libreOfficeMock {
|
||||||
|
m := &libreOfficeMock{
|
||||||
|
pdfMock: func(ctx context.Context, logger *zap.Logger, input, outputPath string, options Options) error {
|
||||||
|
return ErrCoreDumped
|
||||||
|
},
|
||||||
|
}
|
||||||
|
m.pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
||||||
|
return m
|
||||||
|
}(),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
err := tc.mock.pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
||||||
|
|
||||||
err := mock.pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
if !tc.expectError && err != nil {
|
||||||
if err != nil {
|
t.Fatalf("expected no error from libreOfficeMock.pdf but got: %v", err)
|
||||||
t.Errorf("expected no error from libreOfficeMock.pdf, but got: %v", err)
|
}
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected error from libreOfficeMock.pdf but got none")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user