| /* |
| 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) |
| } |