Migrate old logs to new database schema (#1828)

address new models based on #1802
This commit is contained in:
6543 2023-06-12 22:43:14 +02:00 committed by GitHub
parent 609ba481b5
commit 5139624cf8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 221 additions and 24 deletions

View file

@ -144,11 +144,11 @@ test-cli: ## Test cli code
go test -race -cover -coverprofile cli-coverage.out -timeout 30s github.com/woodpecker-ci/woodpecker/cmd/cli github.com/woodpecker-ci/woodpecker/cli/... go test -race -cover -coverprofile cli-coverage.out -timeout 30s github.com/woodpecker-ci/woodpecker/cmd/cli github.com/woodpecker-ci/woodpecker/cli/...
test-server-datastore: ## Test server datastore test-server-datastore: ## Test server datastore
go test -timeout 30s -run TestMigrate github.com/woodpecker-ci/woodpecker/server/store/... go test -timeout 60s -run TestMigrate github.com/woodpecker-ci/woodpecker/server/store/...
go test -race -timeout 30s -skip TestMigrate github.com/woodpecker-ci/woodpecker/server/store/... go test -race -timeout 30s -skip TestMigrate github.com/woodpecker-ci/woodpecker/server/store/...
test-server-datastore-coverage: ## Test server datastore with coverage report test-server-datastore-coverage: ## Test server datastore with coverage report
go test -race -cover -coverprofile datastore-coverage.out -timeout 30s github.com/woodpecker-ci/woodpecker/server/store/... go test -race -cover -coverprofile datastore-coverage.out -timeout 60s github.com/woodpecker-ci/woodpecker/server/store/...
test-ui: ui-dependencies ## Test UI code test-ui: ui-dependencies ## Test UI code
(cd web/; pnpm run lint) (cd web/; pnpm run lint)

View file

@ -241,6 +241,11 @@ var flags = []cli.Flag{
Usage: "status context format", Usage: "status context format",
Value: "{{ .context }}/{{ .event }}/{{ .pipeline }}", Value: "{{ .context }}/{{ .event }}/{{ .pipeline }}",
}, },
&cli.BoolFlag{
EnvVars: []string{"WOODPECKER_MIGRATIONS_ALLOW_LONG"},
Name: "migrations-allow-long",
Value: false,
},
// //
// resource limit parameters // resource limit parameters
// //

View file

@ -356,6 +356,7 @@ func setupEvilGlobals(c *cli.Context, v store.Store, f forge.Forge) {
server.Config.Pipeline.Networks = c.StringSlice("network") server.Config.Pipeline.Networks = c.StringSlice("network")
server.Config.Pipeline.Volumes = c.StringSlice("volume") server.Config.Pipeline.Volumes = c.StringSlice("volume")
server.Config.Pipeline.Privileged = c.StringSlice("escalate") server.Config.Pipeline.Privileged = c.StringSlice("escalate")
server.Config.Server.Migrations.AllowLong = c.Bool("migrations-allow-long")
// prometheus // prometheus
server.Config.Prometheus.AuthToken = c.String("prometheus-auth-token") server.Config.Prometheus.AuthToken = c.String("prometheus-auth-token")

View file

@ -23,8 +23,8 @@ Below are resources requirements for Woodpecker components itself:
| Component | Memory | CPU | | Component | Memory | CPU |
| --------- | ------ | --- | | --------- | ------ | --- |
| Server | 32 MB | 1 | | Server | 200 MB | 1 |
| Agent | 32 MB | 1 | | Agent | 32 MB | 1 |
Note, that those values do not include the operating system or workload (pipelines execution) resources consumption. Note, that those values do not include the operating system or workload (pipelines execution) resources consumption.

View file

@ -19,6 +19,7 @@ Some versions need some changes to the server configuration or the pipeline conf
- Dropped support for [Coding](https://coding.net/) and [Gogs](https://gogs.io). - Dropped support for [Coding](https://coding.net/) and [Gogs](https://gogs.io).
- `/api/queue/resume` & `/api/queue/pause` endpoint methods were changed from `GET` to `POST` - `/api/queue/resume` & `/api/queue/pause` endpoint methods were changed from `GET` to `POST`
- rename `pipeline:` key in your workflow config to `steps:` - rename `pipeline:` key in your workflow config to `steps:`
- If you want to migrate old logs to the new format, watch the error messages on start. If there are none we are good to go, else you have to plan a migration that can take hours. Set `WOODPECKER_ALLOW_LONG_MIGRATION` to true and let it run.
## 0.15.0 ## 0.15.0

View file

@ -67,6 +67,9 @@ var Config = struct {
StatusContextFormat string StatusContextFormat string
SessionExpires time.Duration SessionExpires time.Duration
RootURL string RootURL string
Migrations struct {
AllowLong bool
}
// Open bool // Open bool
// Orgs map[string]struct{} // Orgs map[string]struct{}
// Admins map[string]struct{} // Admins map[string]struct{}

View file

@ -0,0 +1,175 @@
// Copyright 2023 Woodpecker Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package migration
import (
"context"
"encoding/json"
"fmt"
"runtime"
"github.com/rs/zerolog/log"
"github.com/tevino/abool"
"xorm.io/xorm"
"github.com/woodpecker-ci/woodpecker/server"
"github.com/woodpecker-ci/woodpecker/shared/utils"
)
// maxDefaultSqliteItems set the threshold at witch point the migration will fail by default
var maxDefaultSqliteItems019 = 5000
// perPage019 set the size of the slice to read per page
var perPage019 = 100
type oldLogs019 struct {
ID int64 `xorm:"pk autoincr 'log_id'"`
StepID int64 `xorm:"UNIQUE 'log_step_id'"`
Data []byte `xorm:"LONGBLOB 'log_data'"`
}
func (oldLogs019) TableName() string {
return "logs"
}
type oldLogEntry019 struct {
Step string `json:"step,omitempty"`
Time int64 `json:"time,omitempty"`
Type int `json:"type,omitempty"`
Pos int `json:"pos,omitempty"`
Out string `json:"out,omitempty"`
}
type newLogEntry019 struct {
ID int64 `xorm:"pk autoincr 'id'"`
StepID int64 `xorm:"'step_id'"`
Time int64
Line int
Data []byte `xorm:"LONGBLOB"`
Created int64 `xorm:"created"`
Type int
}
func (newLogEntry019) TableName() string {
return "log_entries"
}
var initLogsEntriesTable = task{
name: "init-log_entries",
required: true,
fn: func(sess *xorm.Session) error {
return sess.Sync(new(newLogEntry019))
},
}
var migrateLogs2LogEntries = task{
name: "migrate-logs-to-log_entries",
required: false,
engineFn: func(e *xorm.Engine) error {
// make sure old logs table exists
if exist, err := e.IsTableExist(new(oldLogs019)); !exist || err != nil {
return err
}
// first we check if we have just 1000 entries to migrate
toMigrate, err := e.Count(new(oldLogs019))
if err != nil {
return err
}
if toMigrate > int64(maxDefaultSqliteItems019) && !server.Config.Server.Migrations.AllowLong {
return fmt.Errorf("Migrating logs to log_entries is skipped, as we have %d entries to convert. Set 'WOODPECKER_MIGRATIONS_ALLOW_LONG' to 'true' to migrate anyway", toMigrate)
}
if err := e.Sync(new(oldLogs019)); err != nil {
return err
}
page := 0
logs := make([]*oldLogs019, 0, perPage019)
logEntries := make([]*oldLogEntry019, 0, 50)
sigterm := abool.New()
ctx, cancelCtx := context.WithCancelCause(context.Background())
defer cancelCtx(nil)
_ = utils.WithContextSigtermCallback(ctx, func() {
log.Info().Msg("ctrl+c received, stopping current migration")
sigterm.Set()
})
for {
if sigterm.IsSet() {
return fmt.Errorf("migration 'migrate-logs-to-log_entries' gracefully aborted")
}
sess := e.NewSession().NoCache()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
logs = logs[:0]
err := sess.Limit(perPage019).Find(&logs)
if err != nil {
return err
}
log.Trace().Msgf("migrate-logs-to-log_entries: process page %d", page)
for _, l := range logs {
logEntries = logEntries[:0]
if err := json.Unmarshal(l.Data, &logEntries); err != nil {
return err
}
time := int64(0)
for _, logEntry := range logEntries {
if logEntry.Time > time {
time = logEntry.Time
}
log := &newLogEntry019{
StepID: l.StepID,
Data: []byte(logEntry.Out),
Line: logEntry.Pos,
Time: time,
Type: logEntry.Type,
}
if _, err := sess.Insert(log); err != nil {
return err
}
}
if _, err := sess.Delete(l); err != nil {
return err
}
}
if err := sess.Commit(); err != nil {
return err
}
if len(logs) < perPage019 {
break
}
runtime.GC()
page++
}
return e.DropTables("logs")
},
}

View file

@ -50,6 +50,8 @@ var migrationTasks = []*task{
&dropFiles, &dropFiles,
&removeMachineCol, &removeMachineCol,
&dropOldCols, &dropOldCols,
&initLogsEntriesTable,
&migrateLogs2LogEntries,
} }
var allBeans = []interface{}{ var allBeans = []interface{}{
@ -78,6 +80,8 @@ type task struct {
name string name string
required bool required bool
fn func(sess *xorm.Session) error fn func(sess *xorm.Session) error
// engineFn does manage session on it's own. only use it if you really need to
engineFn func(e *xorm.Engine) error
} }
// initNew create tables for new instance // initNew create tables for new instance
@ -153,36 +157,44 @@ func runTasks(e *xorm.Engine, tasks []*task) error {
} }
log.Trace().Msgf("start migration task '%s'", task.name) log.Trace().Msgf("start migration task '%s'", task.name)
sess := e.NewSession().NoCache() aliveMsgCancel := showBeAliveSign(task.name)
defer sess.Close() defer aliveMsgCancel(nil)
if err := sess.Begin(); err != nil { var taskErr error
return err
}
if task.fn != nil { if task.fn != nil {
aliveMsgCancel := showBeAliveSign(task.name) sess := e.NewSession().NoCache()
if err := task.fn(sess); err != nil { defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if taskErr = task.fn(sess); taskErr != nil {
aliveMsgCancel(nil) aliveMsgCancel(nil)
if err2 := sess.Rollback(); err2 != nil { if err2 := sess.Rollback(); err2 != nil {
err = errors.Join(err, err2) taskErr = errors.Join(taskErr, err2)
} }
if task.required {
return err
}
log.Error().Err(err).Msgf("migration task '%s' failed but is not required", task.name)
continue
} }
aliveMsgCancel(nil) if err := sess.Commit(); err != nil {
log.Debug().Msgf("migration task '%s' done", task.name) return err
}
} else if task.engineFn != nil {
taskErr = task.engineFn(e)
} else { } else {
log.Trace().Msgf("skip migration task '%s'", task.name) log.Trace().Msgf("skip migration task '%s'", task.name)
aliveMsgCancel(nil)
continue
} }
if _, err := sess.Insert(&migrations{task.name}); err != nil { aliveMsgCancel(nil)
return err if taskErr != nil {
if task.required {
return taskErr
}
log.Error().Err(taskErr).Msgf("migration task '%s' failed but is not required", task.name)
continue
} }
if err := sess.Commit(); err != nil { log.Debug().Msgf("migration task '%s' done", task.name)
if _, err := e.Insert(&migrations{task.name}); err != nil {
return err return err
} }