diff --git a/backend/model/spider.go b/backend/model/spider.go
index 61fb53ec..a741fc89 100644
--- a/backend/model/spider.go
+++ b/backend/model/spider.go
@@ -55,10 +55,14 @@ type Spider struct {
GitSyncFrequency string `json:"git_sync_frequency" bson:"git_sync_frequency"` // Git 同步频率
GitSyncError string `json:"git_sync_error" bson:"git_sync_error"` // Git 同步错误
+ // 长任务
+ IsLongTask bool `json:"is_long_task" bson:"is_long_task"` // 是否为长任务
+
// 前端展示
- LastRunTs time.Time `json:"last_run_ts"` // 最后一次执行时间
- LastStatus string `json:"last_status"` // 最后执行状态
- Config entity.ConfigSpiderData `json:"config"` // 可配置爬虫配置
+ LastRunTs time.Time `json:"last_run_ts"` // 最后一次执行时间
+ LastStatus string `json:"last_status"` // 最后执行状态
+ Config entity.ConfigSpiderData `json:"config"` // 可配置爬虫配置
+ LatestTasks []Task `json:"latest_tasks"` // 最近任务列表
// 时间
CreateTs time.Time `json:"create_ts" bson:"create_ts"`
@@ -124,6 +128,18 @@ func (spider *Spider) GetLastTask() (Task, error) {
return tasks[0], nil
}
+// 爬虫正在运行的任务
+func (spider *Spider) GetLatestTasks(latestN int) (tasks []Task, err error) {
+ tasks, err = GetTaskList(bson.M{"spider_id": spider.Id}, 0, latestN, "-create_ts")
+ if err != nil {
+ return tasks, err
+ }
+ if tasks == nil {
+ return tasks, err
+ }
+ return tasks, nil
+}
+
// 删除爬虫
func (spider *Spider) Delete() error {
s, c := database.GetCol("spiders")
@@ -157,9 +173,18 @@ func GetSpiderList(filter interface{}, skip int, limit int, sortStr string) ([]S
continue
}
+ // 获取正在运行的爬虫
+ latestTasks, err := spider.GetLatestTasks(50) // TODO: latestN 暂时写死,后面加入数据库
+ if err != nil {
+ log.Errorf(err.Error())
+ debug.PrintStack()
+ continue
+ }
+
// 赋值
spiders[i].LastRunTs = task.CreateTs
spiders[i].LastStatus = task.Status
+ spiders[i].LatestTasks = latestTasks
}
count, _ := c.Find(filter).Count()
diff --git a/backend/routes/spider.go b/backend/routes/spider.go
index 39314970..dc84f462 100644
--- a/backend/routes/spider.go
+++ b/backend/routes/spider.go
@@ -35,13 +35,23 @@ func GetSpiderList(c *gin.Context) {
sortKey, _ := c.GetQuery("sort_key")
sortDirection, _ := c.GetQuery("sort_direction")
- // 筛选
+ // 筛选-名称
filter := bson.M{
"name": bson.M{"$regex": bson.RegEx{Pattern: keyword, Options: "im"}},
}
+
+ // 筛选-类型
if t != "" && t != "all" {
filter["type"] = t
}
+
+ // 筛选-是否为长任务
+ if t == "long-task" {
+ delete(filter, "type")
+ filter["is_long_task"] = true
+ }
+
+ // 筛选-项目
if pid == "" {
// do nothing
} else if pid == constants.ObjectIdNull {
diff --git a/backend/services/task.go b/backend/services/task.go
index e940b325..76aeed83 100644
--- a/backend/services/task.go
+++ b/backend/services/task.go
@@ -350,10 +350,9 @@ func SaveTaskResultCount(id string) func() {
func ExecuteTask(id int) {
if flag, ok := LockList.Load(id); ok {
if flag.(bool) {
- log.Debugf(GetWorkerPrefix(id) + "正在执行任务...")
+ log.Debugf(GetWorkerPrefix(id) + "running tasks...")
return
}
-
}
// 上锁
@@ -378,6 +377,7 @@ func ExecuteTask(id int) {
// 节点队列
queueCur := "tasks:node:" + node.Id.Hex()
+
// 节点队列任务
var msg string
if msg, err = database.RedisClient.LPop(queueCur); err != nil {
@@ -387,6 +387,7 @@ func ExecuteTask(id int) {
}
}
+ // 如果没有获取到任务,返回
if msg == "" {
return
}
@@ -504,6 +505,8 @@ func ExecuteTask(id int) {
log.Errorf(GetWorkerPrefix(id) + err.Error())
return
}
+
+ // 统计数据
t.Status = constants.StatusFinished // 任务状态: 已完成
t.FinishTs = time.Now() // 结束时间
t.RuntimeDuration = t.FinishTs.Sub(t.StartTs).Seconds() // 运行时长
@@ -849,6 +852,14 @@ func SendNotifications(u model.User, t model.Task, s model.Spider) {
}
}
+func UnlockLongTask(s model.Spider, n model.Node) {
+ if s.IsLongTask {
+ colName := "long-tasks"
+ key := fmt.Sprintf("%s:%s", s.Id.Hex(), n.Id.Hex())
+ _ = database.RedisClient.HDel(colName, key)
+ }
+}
+
func InitTaskExecutor() error {
c := cron.New(cron.WithSeconds())
Exec = &Executor{
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index d66691c4..93d41827 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -47,6 +47,10 @@ export default {
// get latest version
await this.$store.dispatch('version/getLatestRelease')
+
+ // remove loading-placeholder
+ const elLoading = document.querySelector('#loading-placeholder')
+ elLoading.remove()
}
}
diff --git a/frontend/src/components/Common/CrawlConfirmDialog.vue b/frontend/src/components/Common/CrawlConfirmDialog.vue
index e38af787..1d5e2d7a 100644
--- a/frontend/src/components/Common/CrawlConfirmDialog.vue
+++ b/frontend/src/components/Common/CrawlConfirmDialog.vue
@@ -68,7 +68,7 @@
我已阅读并同意 《免责声明》 所有内容
-
+
跳转到任务详情页
@@ -172,6 +172,8 @@ export default {
this.$router.push('/tasks/' + id)
this.$st.sendEv('爬虫确认', '跳转到任务详情')
}
+
+ this.$emit('confirm')
})
},
onClickDisclaimer () {
diff --git a/frontend/src/components/Status/StatusTag.vue b/frontend/src/components/Status/StatusTag.vue
index 0278f02e..273ba8e4 100644
--- a/frontend/src/components/Status/StatusTag.vue
+++ b/frontend/src/components/Status/StatusTag.vue
@@ -21,7 +21,8 @@ export default {
running: { label: 'Running', type: 'warning' },
finished: { label: 'Finished', type: 'success' },
error: { label: 'Error', type: 'danger' },
- cancelled: { label: 'Cancelled', type: 'info' }
+ cancelled: { label: 'Cancelled', type: 'info' },
+ abnormal: { label: 'Abnormal', type: 'danger' }
}
}
},
@@ -43,6 +44,8 @@ export default {
icon () {
if (this.status === 'finished') {
return 'el-icon-check'
+ } else if (this.status === 'pending') {
+ return 'el-icon-loading'
} else if (this.status === 'running') {
return 'el-icon-loading'
} else if (this.status === 'error') {
@@ -50,7 +53,7 @@ export default {
} else if (this.status === 'cancelled') {
return 'el-icon-video-pause'
} else if (this.status === 'abnormal') {
- return 'el-icon-question'
+ return 'el-icon-warning'
} else {
return 'el-icon-question'
}
diff --git a/frontend/src/i18n/zh.js b/frontend/src/i18n/zh.js
index f311a093..5c7e03e2 100644
--- a/frontend/src/i18n/zh.js
+++ b/frontend/src/i18n/zh.js
@@ -39,6 +39,7 @@ export default {
Error: '错误',
NA: '未知',
Cancelled: '已取消',
+ Abnormal: '异常',
// 操作
Add: '添加',
@@ -214,7 +215,8 @@ export default {
'SSH Public Key': 'SSH 公钥',
'Is Long Task': '是否为长任务',
'Long Task': '长任务',
- 'Running Tasks': '运行任务数',
+ 'Running Task Count': '运行中的任务数',
+ 'Running Tasks': '运行中的任务',
// 爬虫列表
'Name': '名称',
diff --git a/frontend/src/store/modules/task.js b/frontend/src/store/modules/task.js
index 595ec7b0..85270729 100644
--- a/frontend/src/store/modules/task.js
+++ b/frontend/src/store/modules/task.js
@@ -174,10 +174,13 @@ const actions = {
link.remove()
},
cancelTask ({ state, dispatch }, id) {
- return request.post(`/tasks/${id}/cancel`)
- .then(() => {
- dispatch('getTaskData', id)
- })
+ return new Promise(resolve => {
+ request.post(`/tasks/${id}/cancel`)
+ .then(res => {
+ dispatch('getTaskData', id)
+ resolve(res)
+ })
+ })
}
}
diff --git a/frontend/src/views/spider/SpiderList.vue b/frontend/src/views/spider/SpiderList.vue
index f9f99b6c..2ddd2369 100644
--- a/frontend/src/views/spider/SpiderList.vue
+++ b/frontend/src/views/spider/SpiderList.vue
@@ -143,11 +143,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{$t('Tasks')}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{$t('Offline')}}
+ {{$t('Online')}}
+ {{$t('Unavailable')}}
+
+
+
+
+
+ {{$t('Ok')}}
+
+
+
+
@@ -226,6 +338,35 @@
+
+
+
+
+ {{$t('Pending')}}
+
+
+
+ {{$t('Running')}}
+
+
+
+ {{$t('Finished')}}
+
+
+
+ {{$t('Error')}}
+
+
+
+ {{$t('Cancelled')}}
+
+
+
+ {{$t('Abnormal')}}
+
+
+
+
-
- {{scope.row[col.name] ? scope.row[col.name].length : '0'}} / {{activeNodeList.length}}
+
+
+ {{getTaskCountByStatus(scope.row, 'pending')}}
+
+
+
+ {{getTaskCountByStatus(scope.row, 'running')}}
+
+
+
+ {{getTaskCountByStatus(scope.row, 'finished')}}
+
+
+
+ {{getTaskCountByStatus(scope.row, 'error')}}
+
+
+
+ {{getTaskCountByStatus(scope.row, 'cancelled')}}
+
+
+
+ {{getTaskCountByStatus(scope.row, 'abnormal')}}
@@ -338,7 +526,7 @@
>
-
+
+
+
+
@@ -401,7 +597,9 @@ export default {
dialogVisible: false,
addDialogVisible: false,
crawlConfirmDialogVisible: false,
+ isRunningTasksDialogVisible: false,
activeSpiderId: undefined,
+ activeSpider: undefined,
filter: {
project_id: '',
keyword: '',
@@ -539,7 +737,8 @@ export default {
this.$utils.tour.nextStep('spider-list-add', currentStep)
}
},
- handle: undefined
+ handle: undefined,
+ activeSpiderTaskStatus: 'running'
}
},
computed: {
@@ -576,11 +775,11 @@ export default {
columns.push({ name: 'type', label: 'Spider Type', width: '120', sortable: true })
columns.push({ name: 'is_long_task', label: 'Is Long Task', width: '80' })
columns.push({ name: 'is_scrapy', label: 'Is Scrapy', width: '80' })
+ columns.push({ name: 'latest_tasks', label: 'Latest Tasks', width: '180' })
columns.push({ name: 'last_status', label: 'Last Status', width: '120' })
columns.push({ name: 'last_run_ts', label: 'Last Run', width: '140' })
columns.push({ name: 'update_ts', label: 'Update Time', width: '140' })
columns.push({ name: 'create_ts', label: 'Create Time', width: '140' })
- columns.push({ name: 'running_tasks', label: 'Running Tasks', width: '120' })
columns.push({ name: 'remark', label: 'Remark', width: '140' })
return columns
},
@@ -686,12 +885,6 @@ export default {
onAddDialogClose () {
this.addDialogVisible = false
},
- onAddCustomizedDialogClose () {
- this.addCustomizedDialogVisible = false
- },
- onAddConfigurableDialogClose () {
- this.addConfigurableDialogVisible = false
- },
onEdit (row) {
this.isEditMode = true
this.$store.commit('spider/SET_SPIDER_FORM', row)
@@ -719,6 +912,11 @@ export default {
this.activeSpiderId = row._id
this.$st.sendEv('爬虫列表', '点击运行')
},
+ onCrawlConfirm () {
+ setTimeout(() => {
+ this.getList()
+ }, 1000)
+ },
onView (row, ev) {
ev.stopPropagation()
this.$router.push('/spiders/' + row._id)
@@ -768,21 +966,6 @@ export default {
callback(data)
})
},
- onAddConfigurableSiteSelect (item) {
- this.spiderForm.site = item._id
- },
- onAddConfigurableSpider () {
- this.$refs['addConfigurableForm'].validate(res => {
- if (res) {
- this.addConfigurableLoading = true
- this.$store.dispatch('spider/addSpider')
- .finally(() => {
- this.addConfigurableLoading = false
- this.addConfigurableDialogVisible = false
- })
- }
- })
- },
onUploadSuccess (res) {
// clear fileList
this.fileList = []
@@ -836,14 +1019,56 @@ export default {
project_id: this.filter.project_id
}
await this.$store.dispatch('spider/getSpiderList', params)
+
+ // 更新当前爬虫(任务列表)
+ this.updateActiveSpider()
},
- getRunningTasksStatusType (row) {
- if (!row.running_tasks || row.running_tasks.length === 0) {
- return 'info'
- } else if (this.activeNodeList && row.running_tasks.length === this.activeNodeList.length) {
- return 'warning'
- } else {
- return 'warning'
+ getTasksByStatus (row, status) {
+ if (!row.latest_tasks) return []
+ return row.latest_tasks.filter(d => d.status === status)
+ },
+ getTaskCountByStatus (row, status) {
+ return this.getTasksByStatus(row, status).length
+ },
+ updateActiveSpider () {
+ if (this.activeSpider) {
+ for (let i = 0; i < this.spiderList.length; i++) {
+ const spider = this.spiderList[i]
+ if (this.activeSpider._id === spider._id) {
+ this.activeSpider = spider
+ }
+ }
+ }
+ },
+ onViewRunningTasks (row, ev) {
+ ev.stopPropagation()
+ this.activeSpider = row
+ this.isRunningTasksDialogVisible = true
+ },
+ getTasksByNode (row) {
+ if (!this.activeSpider.latest_tasks) {
+ return []
+ }
+ return this.activeSpider.latest_tasks
+ .filter(d => d.node_id === row._id && d.status === this.activeSpiderTaskStatus)
+ .map(d => {
+ d = JSON.parse(JSON.stringify(d))
+ d.create_ts = d.create_ts.match('^0001') ? 'NA' : dayjs(d.create_ts).format('YYYY-MM-DD HH:mm:ss')
+ d.start_ts = d.start_ts.match('^0001') ? 'NA' : dayjs(d.start_ts).format('YYYY-MM-DD HH:mm:ss')
+ d.finish_ts = d.finish_ts.match('^0001') ? 'NA' : dayjs(d.finish_ts).format('YYYY-MM-DD HH:mm:ss')
+ return d
+ })
+ },
+ onViewTask (row) {
+ this.$router.push(`/tasks/${row._id}`)
+ this.$st.sendEv('爬虫列表', '任务列表', '查看任务')
+ },
+ async onStop (row, ev) {
+ ev.stopPropagation()
+ const res = await this.$store.dispatch('task/cancelTask', row._id)
+ if (!res.data.error) {
+ this.$message.success(`Task "${row._id}" has been sent signal to stop`)
+ this.getList()
}
}
},
@@ -856,6 +1081,9 @@ export default {
this.filter.project_id = this.$route.params.project_id
}
+ // fetch node list
+ await this.$store.dispatch('node/getNodeList')
+
// fetch spider list
await this.getList()
@@ -969,4 +1197,12 @@ export default {
.actions {
text-align: right;
}
+
+ .el-table >>> .latest-tasks .el-tag {
+ margin: 3px 3px 0 0;
+ }
+
+ .legend .el-tag {
+ margin-right: 5px;
+ }