1
0
Fork 0

Merge pull request #1232 from rqlite/auto-backup

Auto backup improvements
master
Philip O'Toole 1 year ago committed by GitHub
commit 823b76051e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,6 +1,6 @@
## 7.15.0 (unreleased)
### New features
- [PR #1229](https://github.com/rqlite/rqlite/pull/1229): Add support for automatic backups to AWS S3.
- [PR #1229](https://github.com/rqlite/rqlite/pull/1229), [PR #1232](https://github.com/rqlite/rqlite/pull/1232): Add support for automatic backups to AWS S3.
## 7.14.3 (April 25th 2023)
### Implementation changes and bug fixes

@ -28,7 +28,6 @@ import (
"github.com/rqlite/rqlite/command"
sql "github.com/rqlite/rqlite/db"
rlog "github.com/rqlite/rqlite/log"
"github.com/rqlite/rqlite/upload"
)
var (
@ -934,32 +933,13 @@ func (s *Store) Backup(br *command.BackupRequest, dst io.Writer) (retErr error)
}
// Provide implements the uploader Provider interface, allowing the
// Store to be used as a DataProvider for an uploader. It returns
// a io.ReadCloser that can be used to read a copy of the entire database.
// When the ReadCloser is closed, the resources backing it are cleaned up.
func (s *Store) Provide() (io.ReadCloser, error) {
if !s.open {
return nil, ErrNotOpen
}
tempFile, err := os.CreateTemp("", "rqlite-upload-")
if err != nil {
return nil, err
}
if err := tempFile.Close(); err != nil {
return nil, err
}
if err := s.db.Backup(tempFile.Name()); err != nil {
return nil, err
}
fd, err := upload.NewAutoDeleteFile(tempFile.Name())
if err != nil {
return nil, err
// Store to be used as a DataProvider for an uploader.
func (s *Store) Provide(path string) error {
if err := s.db.Backup(path); err != nil {
return err
}
stats.Add(numProvides, 1)
return fd, nil
return nil
}
// Loads an entire SQLite file into the database, sending the request

@ -3,7 +3,6 @@ package store
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"math/rand"
"net"
@ -906,23 +905,13 @@ func Test_SingleNodeProvide(t *testing.T) {
t.Fatalf("unexpected results for query\nexp: %s\ngot: %s", exp, got)
}
rc, err := s0.Provide()
tempFile := mustCreateTempFile()
defer os.Remove(tempFile)
err = s0.Provide(tempFile)
if err != nil {
t.Fatalf("store failed to provide: %s", err.Error())
}
f, err := os.CreateTemp("", "rqlite-store-test")
if err != nil {
t.Fatalf("failed to create temp file: %s", err.Error())
}
defer os.Remove(f.Name())
defer f.Close()
_, err = io.Copy(f, rc)
if err != nil {
t.Fatalf("failed to copy data from store: %s", err.Error())
}
// Load the provided data into a new store and check it.
s1, ln := mustNewStore(t, true)
defer ln.Close()
@ -938,7 +927,7 @@ func Test_SingleNodeProvide(t *testing.T) {
t.Fatalf("Error waiting for leader: %s", err)
}
err = s1.Load(loadRequestFromFile(f.Name()))
err = s1.Load(loadRequestFromFile(tempFile))
if err != nil {
t.Fatalf("failed to load provided SQLite data: %s", err.Error())
}
@ -973,7 +962,9 @@ func Test_SingleNodeInMemProvideNoData(t *testing.T) {
t.Fatalf("Error waiting for leader: %s", err)
}
_, err := s.Provide()
tmpFile := mustCreateTempFile()
defer os.Remove(tmpFile)
err := s.Provide(tmpFile)
if err != nil {
t.Fatalf("store failed to provide: %s", err.Error())
}
@ -2308,6 +2299,15 @@ func (m *mockListener) Close() error { return m.ln.Close() }
func (m *mockListener) Addr() net.Addr { return m.ln.Addr() }
func mustCreateTempFile() string {
f, err := os.CreateTemp("", "rqlite-temp")
if err != nil {
panic("failed to create temporary file")
}
f.Close()
return f.Name()
}
func mustWriteFile(path, contents string) {
err := os.WriteFile(path, []byte(contents), 0644)
if err != nil {

@ -111,7 +111,8 @@ class TestAutoBackupS3(unittest.TestCase):
node.start()
node.wait_for_leader()
node.execute('CREATE TABLE foo (id INTEGER NOT NULL PRIMARY KEY, name TEXT)')
node.execute('INSERT INTO foo(name) VALUES("fiona")')
for i in range(1000):
node.execute('INSERT INTO foo(name) VALUES("fiona")')
node.wait_for_all_fsm()
time.sleep(5)
@ -122,10 +123,10 @@ class TestAutoBackupS3(unittest.TestCase):
backup_file = gunzip_file(compressed_backup_file)
conn = sqlite3.connect(backup_file)
c = conn.cursor()
c.execute('SELECT * FROM foo')
c.execute('SELECT count(*) FROM foo WHERE name="fiona"')
rows = c.fetchall()
self.assertEqual(len(rows), 1)
self.assertEqual(rows[0][1], 'fiona')
self.assertEqual(rows[0][0], 1000)
conn.close()
deprovision_node(node)

@ -1,28 +0,0 @@
package upload
import (
"os"
)
// AutoDeleteFile is a wrapper around os.File that deletes the file when it is
// closed.
type AutoDeleteFile struct {
*os.File
}
// Close implements the io.Closer interface
func (f *AutoDeleteFile) Close() error {
if err := f.File.Close(); err != nil {
return err
}
return os.Remove(f.Name())
}
// NewAutoDeleteFile takes a filename and wraps it in an AutoDeleteFile
func NewAutoDeleteFile(path string) (*AutoDeleteFile, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
return &AutoDeleteFile{f}, nil
}

@ -1,57 +0,0 @@
package upload
import (
"os"
"testing"
)
func Test_NewAutoDeleteTempFile(t *testing.T) {
adFile, err := NewAutoDeleteFile(mustCreateTempFilename())
if err != nil {
t.Fatalf("NewAutoDeleteFile() failed: %v", err)
}
defer adFile.Close()
if _, err := os.Stat(adFile.Name()); os.IsNotExist(err) {
t.Fatalf("Expected file to exist: %s", adFile.Name())
}
}
func Test_AutoDeleteFile_Name(t *testing.T) {
name := mustCreateTempFilename()
adFile, err := NewAutoDeleteFile(name)
if err != nil {
t.Fatalf("NewAutoDeleteFile() failed: %v", err)
}
defer adFile.Close()
if adFile.Name() != name {
t.Fatalf("Expected Name() to return %s, got %s", name, adFile.Name())
}
}
func Test_AutoDeleteFile_Close(t *testing.T) {
adFile, err := NewAutoDeleteFile(mustCreateTempFilename())
if err != nil {
t.Fatalf("NewAutoDeleteFile() failed: %v", err)
}
filename := adFile.Name()
err = adFile.Close()
if err != nil {
t.Fatalf("Close() failed: %v", err)
}
if _, err := os.Stat(filename); !os.IsNotExist(err) {
t.Fatalf("Expected file to be deleted after Close(): %s", filename)
}
}
func mustCreateTempFilename() string {
f, err := os.CreateTemp("", "autodeletefile_test")
if err != nil {
panic(err)
}
f.Close()
return f.Name()
}

@ -0,0 +1,39 @@
package upload
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"io"
"os"
)
// SHA256Sum is a SHA256 hash.
type SHA256Sum []byte
// String returns the hex-encoded string representation of the SHA256Sum.
func (s SHA256Sum) String() string {
return hex.EncodeToString(s)
}
// Equals returns true if the SHA256Sum is equal to the other.
func (s SHA256Sum) Equals(other SHA256Sum) bool {
return bytes.Equal(s, other)
}
// FileSHA256 returns the SHA256 hash of the file at the given path.
func FileSHA256(filePath string) (SHA256Sum, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
hasher := sha256.New()
if _, err := io.Copy(hasher, file); err != nil {
return nil, err
}
hash := hasher.Sum(nil)
return SHA256Sum(hash), nil
}

@ -0,0 +1,115 @@
package upload
import (
"bytes"
"crypto/sha256"
"io"
"io/ioutil"
"os"
"testing"
)
func TestSHA256SumString(t *testing.T) {
tests := []struct {
name string
input SHA256Sum
expected string
}{
{
name: "empty hash",
input: SHA256Sum([]byte{}),
expected: "",
},
{
name: "non-empty hash",
input: SHA256Sum([]byte{0x12, 0x34, 0x56}),
expected: "123456",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.input.String()
if output != tt.expected {
t.Errorf("Expected: %s, got: %s", tt.expected, output)
}
})
}
}
func TestSHA256SumEquals(t *testing.T) {
tests := []struct {
name string
input1 SHA256Sum
input2 SHA256Sum
expected bool
}{
{
name: "equal hashes",
input1: SHA256Sum([]byte{0x12, 0x34, 0x56}),
input2: SHA256Sum([]byte{0x12, 0x34, 0x56}),
expected: true,
},
{
name: "unequal hashes",
input1: SHA256Sum([]byte{0x12, 0x34, 0x56}),
input2: SHA256Sum([]byte{0x12, 0x34, 0x57}),
expected: false,
},
{
name: "unequal hashes with one being nil",
input1: SHA256Sum([]byte{0x12, 0x34, 0x56}),
input2: nil,
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := tt.input1.Equals(tt.input2)
if output != tt.expected {
t.Errorf("Expected: %v, got: %v", tt.expected, output)
}
})
}
}
func TestFileSha256(t *testing.T) {
data := []byte("Test file content")
tempFileName := mustWriteDataTempFile(data)
defer os.Remove(tempFileName)
// Calculate the SHA256 sum of the file contents using crypto/sha256 calls
hasher := sha256.New()
if _, err := io.Copy(hasher, bytes.NewReader(data)); err != nil {
t.Fatalf("Error calculating hash with crypto/sha256: %v", err)
}
expectedHash := hasher.Sum(nil)
// Call fileSha256 and check that it returns the same hash as the direct call
hash, err := FileSHA256(tempFileName)
if err != nil {
t.Fatalf("Error calling fileSha256: %v", err)
}
if !hash.Equals(SHA256Sum(expectedHash)) {
t.Errorf("Expected: %x, got: %s", expectedHash, hash)
}
}
func mustWriteDataTempFile(data []byte) string {
tempFile, err := ioutil.TempFile("", "uploader_test")
if err != nil {
panic("Error creating temp file: " + err.Error())
}
if _, err := tempFile.Write(data); err != nil {
panic("Error writing to temp file: " + err.Error())
}
if err := tempFile.Close(); err != nil {
panic("Error closing temp file: " + err.Error())
}
return tempFile.Name()
}

@ -1,7 +1,6 @@
package upload
import (
"bytes"
"compress/gzip"
"context"
"expvar"
@ -19,21 +18,21 @@ type StorageClient interface {
}
// DataProvider is an interface for providing data to be uploaded. The Uploader
// service will call Provide() to get a reader for the data to be uploaded. Once
// the upload completes the reader will be closed, regardless of whether the
// upload succeeded or failed.
// service will call Provide() to have the data-for-upload to be written to the
// to the file specified by path.
type DataProvider interface {
Provide() (io.ReadCloser, error)
Provide(path string) error
}
// stats captures stats for the Uploader service.
var stats *expvar.Map
const (
numUploadsOK = "num_uploads_ok"
numUploadsFail = "num_uploads_fail"
totalUploadBytes = "total_upload_bytes"
lastUploadBytes = "last_upload_bytes"
numUploadsOK = "num_uploads_ok"
numUploadsFail = "num_uploads_fail"
numUploadsSkipped = "num_uploads_skipped"
totalUploadBytes = "total_upload_bytes"
lastUploadBytes = "last_upload_bytes"
UploadCompress = true
UploadNoCompress = false
@ -49,6 +48,7 @@ func ResetStats() {
stats.Init()
stats.Add(numUploadsOK, 0)
stats.Add(numUploadsFail, 0)
stats.Add(numUploadsSkipped, 0)
stats.Add(totalUploadBytes, 0)
stats.Add(lastUploadBytes, 0)
}
@ -63,6 +63,12 @@ type Uploader struct {
logger *log.Logger
lastUploadTime time.Time
lastUploadDuration time.Duration
lastSum SHA256Sum
// disableSumCheck is used for testing purposes to disable the check that
// prevents uploading the same data twice.
disableSumCheck bool
}
// NewUploader creates a new Uploader service.
@ -93,6 +99,10 @@ func (u *Uploader) Start(ctx context.Context, isUploadEnabled func() bool) {
return
case <-ticker.C:
if !isUploadEnabled() {
// Reset the lastSum so that the next time we're enabled upload will
// happen. We do this to be conservative, as we don't know what was
// happening while upload was disabled.
u.lastSum = nil
continue
}
if err := u.upload(ctx); err != nil {
@ -110,38 +120,47 @@ func (u *Uploader) Stats() (map[string]interface{}, error) {
"compress": u.compress,
"last_upload_time": u.lastUploadTime.Format(time.RFC3339),
"last_upload_duration": u.lastUploadDuration.String(),
"last_upload_sum": u.lastSum.String(),
}
return status, nil
}
func (u *Uploader) upload(ctx context.Context) error {
rc, err := u.dataProvider.Provide()
// create a temporary file for the data to be uploaded
filetoUpload, err := tempFilename()
if err != nil {
return err
}
defer rc.Close()
defer os.Remove(filetoUpload)
r := rc.(io.Reader)
if u.compress {
buffer := new(bytes.Buffer)
gw := gzip.NewWriter(buffer)
_, err = io.Copy(gw, rc)
if err != nil {
return err
}
err = gw.Close()
if err != nil {
return err
}
r = buffer
if err := u.dataProvider.Provide(filetoUpload); err != nil {
return err
}
if err := u.compressIfNeeded(filetoUpload); err != nil {
return err
}
sum, err := FileSHA256(filetoUpload)
if err != nil {
return err
}
if !u.disableSumCheck && sum.Equals(u.lastSum) {
stats.Add(numUploadsSkipped, 1)
return nil
}
cr := &countingReader{reader: r}
fd, err := os.Open(filetoUpload)
if err != nil {
return err
}
cr := &countingReader{reader: fd}
startTime := time.Now()
err = u.storageClient.Upload(ctx, cr)
if err != nil {
stats.Add(numUploadsFail, 1)
} else {
u.lastSum = sum
stats.Add(numUploadsOK, 1)
stats.Add(totalUploadBytes, cr.count)
stats.Get(lastUploadBytes).(*expvar.Int).Set(cr.count)
@ -151,6 +170,49 @@ func (u *Uploader) upload(ctx context.Context) error {
return err
}
func (u *Uploader) compressIfNeeded(path string) error {
if !u.compress {
return nil
}
compressedFile, err := tempFilename()
if err != nil {
return err
}
defer os.Remove(compressedFile)
if err = compressFromTo(path, compressedFile); err != nil {
return err
}
return os.Rename(compressedFile, path)
}
func compressFromTo(from, to string) error {
uncompressedFd, err := os.Open(from)
if err != nil {
return err
}
defer uncompressedFd.Close()
compressedFd, err := os.Create(to)
if err != nil {
return err
}
defer compressedFd.Close()
gw := gzip.NewWriter(compressedFd)
_, err = io.Copy(gw, uncompressedFd)
if err != nil {
return err
}
err = gw.Close()
if err != nil {
return err
}
return nil
}
type countingReader struct {
reader io.Reader
count int64
@ -161,3 +223,12 @@ func (c *countingReader) Read(p []byte) (int, error) {
c.count += int64(n)
return n, err
}
func tempFilename() (string, error) {
f, err := os.CreateTemp("", "rqlite-upload")
if err != nil {
return "", err
}
f.Close()
return f.Name(), nil
}

@ -6,7 +6,7 @@ import (
"expvar"
"fmt"
"io"
"strings"
"os"
"sync"
"sync/atomic"
"testing"
@ -110,6 +110,7 @@ func Test_UploaderDoubleUpload(t *testing.T) {
}
dp := &mockDataProvider{data: "my upload data"}
uploader := NewUploader(sc, dp, 100*time.Millisecond, UploadNoCompress)
uploader.disableSumCheck = true // Force upload of the same data
ctx, cancel := context.WithCancel(context.Background())
go uploader.Start(ctx, nil)
@ -180,6 +181,7 @@ func Test_UploaderOKThenFail(t *testing.T) {
}
dp := &mockDataProvider{data: "my upload data"}
uploader := NewUploader(sc, dp, 100*time.Millisecond, UploadNoCompress)
uploader.disableSumCheck = true // Disable because we want to upload twice.
ctx, cancel := context.WithCancel(context.Background())
go uploader.Start(ctx, nil)
@ -299,9 +301,9 @@ type mockDataProvider struct {
err error
}
func (mp *mockDataProvider) Provide() (io.ReadCloser, error) {
func (mp *mockDataProvider) Provide(path string) error {
if mp.err != nil {
return nil, mp.err
return mp.err
}
return io.NopCloser(strings.NewReader(mp.data)), nil
return os.WriteFile(path, []byte(mp.data), 0644)
}

Loading…
Cancel
Save