blob: 7dd7d2370b0fef61401458f47487b8a98af49332 [file]
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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 api
import (
"fmt"
"reflect"
"time"
"github.com/apache/incubator-devlake/core/dal"
"github.com/apache/incubator-devlake/core/errors"
"github.com/apache/incubator-devlake/core/models"
"github.com/apache/incubator-devlake/core/plugin"
"github.com/apache/incubator-devlake/core/utils"
)
// SubtaskCommonArgs is a struct that contains the common arguments for a subtask
type SubtaskCommonArgs struct {
plugin.SubTaskContext
Table string // raw table name
Params any // for filtering rows belonging to the scope (jira board, github repo) of the subtask
SubtaskConfig any // for determining whether the subtask should run in Incremental or Full-Sync mode by comparing with the previous config to see if it changed
BatchSize int // batch size for saving data
}
func (args *SubtaskCommonArgs) GetRawDataTable() string {
return fmt.Sprintf("_raw_%s", args.Table)
}
func (args *SubtaskCommonArgs) GetRawDataParams() string {
if args.Params == nil || reflect.ValueOf(args.Params).IsZero() {
panic(errors.Default.New("Params is nil"))
}
return utils.ToJsonString(args.Params)
}
func (args *SubtaskCommonArgs) GetSubtaskConfig() string {
return utils.ToJsonString(args.SubtaskConfig)
}
func (args *SubtaskCommonArgs) GetBatchSize() int {
if args.BatchSize == 0 {
args.BatchSize = 500
}
return args.BatchSize
}
// SubtaskStateManager manages the state of a subtask. It is used to determine whether
// the subtask should run in incremental mode or full sync mode and what time range to collect.
type SubtaskStateManager struct {
db dal.Dal
state *models.SubtaskState
syncPolicy *models.SyncPolicy
isIncremental bool // tells if the subtask should run in incremental mode or full sync mode
since *time.Time // the start time of the time range to work on
until *time.Time // the end time of the time range to work on
config string // current configuration of the subtask for determining if the subtask should run in incremental or full sync mode
}
// NewSubtaskStateManager create a new SubtaskStateManager
func NewSubtaskStateManager(args *SubtaskCommonArgs) (stateManager *SubtaskStateManager, err errors.Error) {
db := args.GetDal()
// load sync policy and make sure it is not nil
syncPolicy := args.SubTaskContext.TaskContext().SyncPolicy()
if syncPolicy == nil {
syncPolicy = &models.SyncPolicy{}
}
plugin := args.SubTaskContext.TaskContext().GetName()
subtask := args.SubTaskContext.GetName()
params := args.GetRawDataParams()
preState, err := loadPreviousState(db, plugin, subtask, params)
if err != nil {
return
}
isIncremental, since := calculateStateManagerIncrementalMode(syncPolicy, preState, utils.ToJsonString(args.SubtaskConfig))
now := time.Now()
stateManager = &SubtaskStateManager{
db: db,
state: preState,
syncPolicy: syncPolicy,
isIncremental: isIncremental,
since: since,
until: &now,
config: utils.ToJsonString(args.SubtaskConfig),
}
// fallback to the previous timeAfter if no new value
if stateManager.since == nil {
stateManager.since = preState.TimeAfter
}
return
}
func loadPreviousState(db dal.Dal, plugin, subtask, params string) (*models.SubtaskState, errors.Error) {
// load the previous state from the database
preState := &models.SubtaskState{}
err := db.First(preState, dal.Where(`plugin = ? AND subtask =? AND params = ?`, plugin, subtask, params))
if err != nil {
if db.IsErrorNotFound(err) {
preState = &models.SubtaskState{
Plugin: plugin,
Subtask: subtask,
Params: params,
}
} else {
return nil, errors.Default.Wrap(err, "failed to load the previous subtask state")
}
}
return preState, nil
}
// calculateStateManagerIncrementalMode tries to calculate whether state manager should run in incremental mode and returns the state manager's 'since' time.
func calculateStateManagerIncrementalMode(syncPolicy *models.SyncPolicy, preState *models.SubtaskState, newSubtaskConfig string) (bool, *time.Time) {
if preState == nil || syncPolicy == nil {
panic("preState or syncPolicy is nil")
}
// User click 'Collect Data in Full Refresh Mode'
// No matter whether there is a successful pipeline.
if syncPolicy.FullSync {
return false, syncPolicy.TimeAfter
}
// No previous success state means this pipeline has never been executed.
if preState.PrevStartedAt == nil {
return false, syncPolicy.TimeAfter
}
// When subtask config has changed, state manager should NOT in incremental mode.
if subTaskConfigHasChanged(preState, newSubtaskConfig) {
return false, syncPolicy.TimeAfter
}
// There is a sync policy and sync policy is earlier than latest successful pipeline's timeAfter
if syncPolicy.TimeAfter != nil && preState.TimeAfter != nil && syncPolicy.TimeAfter.Before(*preState.TimeAfter) {
return false, syncPolicy.TimeAfter
}
// No need to do a full refresh, run task incrementally.
// New state manager's start time is previous state's finished time.
// But there is no such field, so use previous state's PrevStartedAt time.
return true, preState.PrevStartedAt
}
// subTaskConfigHasChanged checks whether the previous sub-task config is the same as the current sub-task config
// When plugin's scope config changes, Subtask's config may change.
func subTaskConfigHasChanged(preState *models.SubtaskState, newSubtaskConfig string) bool {
if preState == nil {
return true
}
preConfig := preState.PrevConfig
return preConfig != "" && preConfig != newSubtaskConfig
}
func (c *SubtaskStateManager) IsIncremental() bool {
return c.isIncremental
}
func (c *SubtaskStateManager) GetSince() *time.Time {
return c.since
}
func (c *SubtaskStateManager) GetUntil() *time.Time {
return c.until
}
func (c *SubtaskStateManager) Close() errors.Error {
// update timeAfter in the database only for fullsync mode
if !c.isIncremental {
// prefer non-nil value
if c.syncPolicy.TimeAfter != nil {
c.state.TimeAfter = c.syncPolicy.TimeAfter
}
}
// always update the latest success start time
c.state.PrevStartedAt = c.until
c.state.PrevConfig = c.config
return c.db.CreateOrUpdate(c.state)
}