取消为协作式:SDK 会先发布取消结果,再在锁外调用业务 Kill;已进入 Process 的任务在真正退出前仍占用并发额度和去重标记。重试或下一轮复用同一实例额度,不能通过销毁并重建绕过限制;业务不响应取消时,新一轮可能等待旧执行退出。等待中的取消任务会立即移出执行队列,终态入队后释放去重标记。已完成任务仍可按相同 ID 显式重投。
// TestMapReduceJob.go
package main
import (
"encoding/json"
"fmt"
"github.com/alibaba/schedulerx-worker-go/processor"
"github.com/alibaba/schedulerx-worker-go/processor/jobcontext"
"github.com/alibaba/schedulerx-worker-go/processor/mapjob"
"github.com/alibaba/schedulerx-worker-go/processor/taskstatus"
"strconv"
"time"
)
type OrderInfo struct {
Id string `json:"id"`
Value int `json:"value"`
}
func NewOrderInfo(id string, value int) *OrderInfo {
return &OrderInfo{Id: id, Value: value}
}
type TestMapReduceJob struct {
*mapjob.MapReduceJobProcessor
}
func (mr *TestMapReduceJob) Kill(jobCtx *jobcontext.JobContext) error {
//TODO implement me
panic("implement me")
}
// Process the MapReduce model is used to distributed scan orders for timeout confirmation
func (mr *TestMapReduceJob) Process(jobCtx *jobcontext.JobContext) (*processor.ProcessResult, error) {
var (
num = 1000
err error
)
taskName := jobCtx.TaskName()
if jobCtx.JobParameters() != "" {
num, err = strconv.Atoi(jobCtx.JobParameters())
if err != nil {
return nil, err
}
}
if mr.IsRootTask(jobCtx) {
fmt.Println("start root task, taskId=%d", jobCtx.TaskId())
var orderInfos []interface{}
for i := 1; i <= num; i++ {
orderInfos = append(orderInfos, NewOrderInfo(fmt.Sprintf("id_%d", i), i))
}
return mr.Map(jobCtx, orderInfos, "OrderInfo")
} else if taskName == "OrderInfo" {
orderInfo := new(OrderInfo)
if err := json.Unmarshal(jobCtx.Task(), orderInfo); err != nil {
fmt.Printf("task is not OrderInfo, task=%+v\n", jobCtx.Task())
}
fmt.Printf("taskId=%d, orderInfo=%+v\n", jobCtx.TaskId(), orderInfo)
time.Sleep(1 * time.Millisecond)
return processor.NewProcessResult(
processor.WithSucceed(),
processor.WithResult(strconv.Itoa(orderInfo.Value)),
), nil
}
return processor.NewProcessResult(processor.WithFailed()), nil
}
func (mr *TestMapReduceJob) Reduce(jobCtx *jobcontext.JobContext) (*processor.ProcessResult, error) {
allTaskResults := jobCtx.TaskResults()
allTaskStatuses := jobCtx.TaskStatuses()
count := 0
fmt.Printf("reduce: all task count=%d\n", len(allTaskResults))
for key, val := range allTaskResults {
if key == 0 {
continue
}
if allTaskStatuses[key] == taskstatus.TaskStatusSucceed {
num, err := strconv.Atoi(val)
if err != nil {
return nil, err
}
count += num
}
}
fmt.Printf("reduce: succeed task count=%d\n", count)
return processor.NewProcessResult(
processor.WithSucceed(),
processor.WithResult(strconv.Itoa(count)),
), nil
}
4. 注册 client 和 job
package main
import (
"os"
"os/signal"
"syscall"
"time"
"github.com/alibaba/schedulerx-worker-go"
)
func main() {
// This is just an example, the real configuration needs to be obtained from the platform
cfg := &schedulerx.Config{
Endpoint: "acm.aliyun.com",
Namespace: "433d8b23-06e9-408c-aaaa-90d4d1b9a4af",
GroupId: "gojob-test",
AppKey: "xxxxxxx",
}
client, err := schedulerx.GetClient(cfg)
if err != nil {
panic(err)
}
task1 := &HelloWorld{}
task2 := &TestBroadcast{}
task3 := &TestMapJob{
mapjob.NewMapJobProcessor(),
}
task4 := &TestMapReduceJob{
mapjob.NewMapReduceJobProcessor(),
}
// The name HelloWorld registered here must be consistent with the configured on the platform
client.RegisterTask("HelloWorld", task1)
client.RegisterTask("TestBroadcast", task2)
client.RegisterTask("TestMapJob", task3)
client.RegisterTask("TestMapReduceJob", task4)
// wait for the stop signal
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1, syscall.SIGUSR2)
<-c
time.Sleep(time.Second * 5)
}
schedulerx-worker-go 使用文档
背景
SchedulerX 是阿里云提供的分布式任务调度服务(兼容开源 XXL-JOB/ElasticJob/K8s Job/Spring Schedule),支持 Cron 定时、一次性任务、任务编排、分布式数据处理,具有高可用、可视化、可运维、低延时等能力。
schedulerx-worker-go 是 SchedulerX Go 版本的 SDK,该 SDK 由高德贡献。
功能
使用说明
1. 登录 SchedulerX 控制台创建应用,返回配置信息
2. 拉取 Go 版本 SDK
3. 实现接口,编写业务代码
3.1 单机任务
接口如下:
实现接口,参考Demo :
3.2 广播任务
接口如下:
接口描述:
实现接口, 参考Demo:
3.3 Map任务
接口如下:
接口描述:
Push 模式下,任务配置
xattrs.consumerSize控制每个任务实例在单个 Worker 上同时执行的任务数,默认值为 5。超出的任务在 Worker 内排队,等待执行的任务仍可被取消;不同任务实例使用各自的并发额度。这个配置不代表跨 Worker 的总并发,也不限制业务代码内部自行启动的 goroutine 或 API 请求速率。取消为协作式:SDK 会先发布取消结果,再在锁外调用业务
Kill;已进入Process的任务在真正退出前仍占用并发额度和去重标记。重试或下一轮复用同一实例额度,不能通过销毁并重建绕过限制;业务不响应取消时,新一轮可能等待旧执行退出。等待中的取消任务会立即移出执行队列,终态入队后释放去重标记。已完成任务仍可按相同 ID 显式重投。Worker 的实例准入记录最多 65,536 条,已关闭记录保留 1 小时防止迟到消息重新开启旧轮次。开放记录也有 1 小时闲置回收期限;只有执行器已退出且状态队列及发送批次均为空才回收,避免 Master 漏发 Destroy 后永久耗尽容量。容量耗尽时新准入失败;批量推送保持原有批次 ACK,由异步失败状态向真实 Master 回报具体原因。ACK 只表示收到批次,不表示所有子任务已获准执行;状态传递沿用 Actor 通道,没有新增持久投递保证。
兼容性说明
本修复基于上游 v1.0.13。相对该基线,公开 Processor、Client、JobContext 等 API、配置和依赖均未修改;变化位于 internal 实现及可选 protobuf 字段。直接维护 SDK internal 代码的分支需适配 Pool 接口变化。
旧版协议可解码新增可选字段,旧消息也可由新版读取,但这不等于混部时拥有完整跨轮次保护。旧端会忽略新增 Attempt/Serial 元数据;旧清理消息未指定 Attempt 时按当前轮次处理,其中 SerialNum=0 保持旧通配语义。迟到的旧消息仍可能影响新轮次,同一实例的 Master/Worker 全部升级后才能利用新增的清理代次校验。新版 Worker→Master 的单条和批量任务状态携带可选 Attempt,Master 在状态更新、排队及异步刷新前校验它必须匹配当前实例;显式 0 也参与校验。旧版缺少 Attempt 的状态保留原处理行为,因此旧 Worker、旧 Master 或会丢失字段的转发端仍无法获得完整跨重试隔离。此校验不覆盖 Master→Server 的实例终态报告。
从 v1.0.10 升级还包含上游 v1.0.13 的独立变化:最低 Go 声明由 1.20 提升到 1.21、连接池与 Tracing 实现变化、sqlite3 依赖升级。本补丁未新增这些变化,但 v1.0.10 使用方仍需验证升级,不能视为完全无行为变化的替换。
实现接口,参考Demo:
3.4 MapReduce任务
接口如下:
继承Map接口,新增接口如下:
实现接口,参考Demo:
4. 注册 client 和 job
4. 通过控制台创建任务
任务类型选择 golang,任务名称写第 4 步的任务名,比如 HelloWorld
示例
参考 example 目录
Push Map 任务并发与生命周期
Push 模式下,
parallel、grid和batch任务按任务实例在每个 Worker 内共享consumerSize个执行名额;缺省或非正值使用默认值 5。该配置限制同时执行的Process,不阻塞 Actor 接收后续批次或取消消息,也不是跨 Worker 的全局并发限制。 单机和广播任务沿用原来的执行方式。KillProcessor协作取消,SDK 无法强制结束阻塞的用户代码。SDK 自己的重试等待 可被取消,换轮次或销毁也会停止旧重试,但不会提前归还仍在运行的用户代码额度。runtime.Goexit后回收执行名额并继续消费队列;Goexit 会报告失败状态。Attempt和SerialNum,按完整代次处理, 防止旧重试的清理影响新重试。容器绑定所属代次的状态上报队列,旧任务晚结束时 不会新入队到新一轮 handler。Worker→Master 状态也携带可选 Attempt;接收及异步批处理 在修改任务状态、进度、分片状态或持久化之前忽略不匹配的 Attempt,仍确认收到旧批次。 缺字段的旧状态保留原行为;新 Worker 和新 Master 均升级后才能利用这项校验。 兼容旧清理消息缺少 JobId/代次字段;混合版本中, 未携带 Attempt 的消息只能按旧协议的 SerialNum 判断,无法保证跨重试隔离。队列和执行器在空闲后释放,不为等待 Master 清理而长期保留任务载荷或执行 goroutine。 关闭标记在不同路由 Actor 和 Actor 重启之间共享,但不跨 Worker 进程重启持久化。