Skip to content

Commit 7e2a85a

Browse files
committed
improve watch coverage, fix #1354
1 parent f24aa8f commit 7e2a85a

3 files changed

Lines changed: 110 additions & 0 deletions

File tree

pkg/backup/watch.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package backup
22

33
import (
44
"context"
5+
"os"
6+
"os/signal"
57
"regexp"
68
"strings"
9+
"syscall"
710
"time"
811

912
"github.com/Altinity/clickhouse-backup/v2/pkg/config"
@@ -89,6 +92,12 @@ func (b *Backuper) Watch(watchInterval, fullInterval, watchBackupNameTemplate st
8992
}
9093
ctx, cancel = context.WithCancel(ctx)
9194
defer cancel()
95+
// standalone CLI graceful shutdown, server mode cancels the command context via status.Current.CancelAll on SIGTERM
96+
if commandId == status.NotFromAPI {
97+
var stopSignals context.CancelFunc
98+
ctx, stopSignals = signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
99+
defer stopSignals()
100+
}
92101

93102
if !b.ch.IsOpen {
94103
if err = b.ch.Connect(); err != nil {

pkg/backup/watch_schedule.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ func (b *Backuper) newWatchScheduleStates(ctx context.Context) ([]*watchSchedule
7575
if err != nil {
7676
return nil, errors.Wrap(err, "newWatchScheduleStates GetRemoteBackups")
7777
}
78+
return b.buildWatchScheduleStates(ctx, remoteBackups)
79+
}
80+
81+
// buildWatchScheduleStates - build per schedule runtime states from already fetched remote backups
82+
func (b *Backuper) buildWatchScheduleStates(ctx context.Context, remoteBackups []storage.Backup) ([]*watchScheduleState, error) {
83+
var err error
7884
states := make([]*watchScheduleState, 0, len(b.cfg.General.WatchSchedules))
7985
for _, schedule := range b.cfg.General.WatchSchedules {
8086
st := &watchScheduleState{schedule: schedule}

pkg/backup/watch_schedule_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package backup
22

33
import (
4+
"context"
45
"regexp"
56
"testing"
67
"time"
@@ -126,3 +127,97 @@ func TestFindPreviousWatchBackup(t *testing.T) {
126127
assert.Equal(t, "", prevName)
127128
assert.Equal(t, "", prevType)
128129
}
130+
131+
func TestWatchScheduleTemplateRE(t *testing.T) {
132+
ctx := context.Background()
133+
// {type} and {time:layout} placeholders shall turn into \S+
134+
assert.Equal(t, `x-\S+-\S+`, watchScheduleTemplatePrepareRE.ReplaceAllString("x-{type}-{time:20060102150405}", `\S+`))
135+
136+
// template without `{` shall skip ApplyMacros ClickHouse query and compile as anchored regexp
137+
b := NewBackuper(config.DefaultConfig())
138+
templateRE, err := b.watchScheduleTemplateRE(ctx, "daily-static-name")
139+
require.NoError(t, err)
140+
assert.True(t, templateRE.MatchString("daily-static-name"))
141+
assert.False(t, templateRE.MatchString("prefix-daily-static-name-suffix"))
142+
143+
// invalid regexp in template shall trigger compile error
144+
_, err = b.watchScheduleTemplateRE(ctx, "bad[template")
145+
require.Error(t, err)
146+
assert.Contains(t, err.Error(), "can't compile regexp")
147+
}
148+
149+
func newTestScheduleBackuper(schedules config.WatchSchedules) *Backuper {
150+
cfg := config.DefaultConfig()
151+
// no `{` in template, so ApplyMacros shall not query ClickHouse
152+
cfg.General.WatchBackupNameTemplate = "static-name"
153+
cfg.General.WatchSchedules = schedules
154+
return NewBackuper(cfg)
155+
}
156+
157+
func TestBuildWatchScheduleStates(t *testing.T) {
158+
ctx := context.Background()
159+
160+
// success, continue chain from the last matching non-broken remote backup
161+
b := newTestScheduleBackuper(config.WatchSchedules{{Name: "daily", Full: "0 0 * * *", Increment: "*/15 * * * *"}})
162+
remoteBackups := []storage.Backup{
163+
{BackupMetadata: metadata.BackupMetadata{BackupName: "daily-static-name"}},
164+
}
165+
states, err := b.buildWatchScheduleStates(ctx, remoteBackups)
166+
require.NoError(t, err)
167+
require.Len(t, states, 1)
168+
assert.Equal(t, "daily-static-name", states[0].template)
169+
assert.Equal(t, "daily-static-name", states[0].prevBackupName)
170+
assert.Equal(t, "full", states[0].prevBackupType)
171+
assert.NotNil(t, states[0].incrementCron)
172+
173+
// invalid `full` cron expression
174+
b = newTestScheduleBackuper(config.WatchSchedules{{Name: "daily", Full: "garbage"}})
175+
_, err = b.buildWatchScheduleStates(ctx, nil)
176+
require.Error(t, err)
177+
assert.Contains(t, err.Error(), "invalid `full` cron expression")
178+
179+
// invalid `increment` cron expression
180+
b = newTestScheduleBackuper(config.WatchSchedules{{Name: "daily", Full: "0 0 * * *", Increment: "garbage"}})
181+
_, err = b.buildWatchScheduleStates(ctx, nil)
182+
require.Error(t, err)
183+
assert.Contains(t, err.Error(), "invalid `increment` cron expression")
184+
185+
// `full` fires more often than `increment`, warning branch shall not fail state build
186+
b = newTestScheduleBackuper(config.WatchSchedules{{Name: "daily", Full: "*/15 * * * *", Increment: "0 * * * *"}})
187+
states, err = b.buildWatchScheduleStates(ctx, nil)
188+
require.NoError(t, err)
189+
require.Len(t, states, 1)
190+
191+
// invalid regexp in watch_backup_name_template propagates from watchScheduleTemplateRE
192+
b = newTestScheduleBackuper(config.WatchSchedules{{Name: "daily", Full: "0 0 * * *"}})
193+
b.cfg.General.WatchBackupNameTemplate = "bad["
194+
_, err = b.buildWatchScheduleStates(ctx, nil)
195+
require.Error(t, err)
196+
assert.Contains(t, err.Error(), "can't compile regexp")
197+
}
198+
199+
// newTestUnreachableClickHouseBackuper - port 1 is closed, ch.Connect inside GetRemoteBackups shall fail fast
200+
func newTestUnreachableClickHouseBackuper() *Backuper {
201+
cfg := config.DefaultConfig()
202+
cfg.ClickHouse.Host = "127.0.0.1"
203+
cfg.ClickHouse.Port = 1
204+
cfg.General.WatchSchedules = config.WatchSchedules{{Name: "daily", Full: "0 0 * * *"}}
205+
b := NewBackuper(cfg)
206+
// Connect retries forever by design (issue #857), fail on the first Ping error instead of hanging the test
207+
b.ch.BreakConnectOnError = true
208+
return b
209+
}
210+
211+
func TestNewWatchScheduleStatesRemoteError(t *testing.T) {
212+
b := newTestUnreachableClickHouseBackuper()
213+
_, err := b.newWatchScheduleStates(context.Background())
214+
require.Error(t, err)
215+
assert.Contains(t, err.Error(), "newWatchScheduleStates GetRemoteBackups")
216+
}
217+
218+
func TestWatchWithSchedulesStatesError(t *testing.T) {
219+
b := newTestUnreachableClickHouseBackuper()
220+
err := b.watchWithSchedules(context.Background(), "", "", "", nil, "", nil, nil, false, false, false, false, false, false, "test", 0, nil, nil)
221+
require.Error(t, err)
222+
assert.Contains(t, err.Error(), "watchWithSchedules newWatchScheduleStates")
223+
}

0 commit comments

Comments
 (0)