Skip to content

Commit

Permalink
Merge pull request #156 from midwayjs/fix_schedule
Browse files Browse the repository at this point in the history
refactor: support @schedule decorator
  • Loading branch information
czy88840616 committed Feb 28, 2019
2 parents 78ac066 + a634e53 commit e76abb4
Show file tree
Hide file tree
Showing 28 changed files with 203 additions and 503 deletions.
15 changes: 8 additions & 7 deletions docs/en/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,27 +432,28 @@ So that we can inject the config values into the business logic without coupling

### Schedule task

The schedule of midawy is based on [egg schedule](https://eggjs.org/en/basics/schedule.html), and provide more typescript and decorator support. The task all store in `lib/schedule`, every file is single schedule task which can be configured the properties and specify jobs. For example:
The schedule of midway is based on [egg schedule](https://eggjs.org/en/basics/schedule.html), and provide more typescript and decorator support. The task can store in any file like `src/schedule`, it can be configured the properties and specify jobs. For example:

```typescript
// src/lib/schedule/hello.ts
'use strict';

import { schedule } from 'midway';
// src/schedule/hello.ts
import { provide, schedule, CommonSchedule } from 'midway';

@provide()
@schedule({
interval: 2333, // 2.333s interval
type: 'worker', // only run in certain worker
})
export class HelloCron {
export class HelloCron implements CommonSchedule {
// The detail job while times up
async exec(ctx) {
ctx.logger.info(process.pid, 'hello');
}
}
```

PS: The schedule class need `export` to be loadable. And the `.ts` file can `export` multi schedule class except `default class`.
:::tip
It is recommended to use `CommonSchedule` interface to standardize your schedule class.
:::

### Logger inject

Expand Down
14 changes: 7 additions & 7 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,28 +454,28 @@ export class BaseService {

### 注册定时任务

midawy 的定时任务是基于 [egg 定时任务](https://eggjs.org/zh-cn/basics/schedule.html)提供了更多 typescript 以及装饰器方面的支持。将定时任务都统一存放在 lib/schedule 目录下,每一个文件都是一个独立的定时任务,可以配置定时任务的属性和要执行的方法。例如:
midawy 的定时任务是基于 [egg 定时任务](https://eggjs.org/zh-cn/basics/schedule.html)提供了更多 typescript 以及装饰器方面的支持。定时任务可以存放在任意目录,例如 src/schedule 目录下,可以配置定时任务的属性和要执行的方法。例如:

```typescript
// src/lib/schedule/hello.ts
'use strict';

import { provide, schedule } from 'midway';
// src/schedule/hello.ts
import { provide, schedule, CommonSchedule } from 'midway';

@provide()
@schedule({
interval: 2333, // 2.333s 间隔
type: 'worker', // 指定某一个 worker 执行
})
export class HelloCron {
export class HelloCron implements CommonSchedule {
// 定时执行的具体任务
async exec(ctx) {
ctx.logger.info(process.pid, 'hello');
}
}
```

PS: 定时任务类需 `export` 导出才会被加载,并且一个 `.ts` 文件可以 `export` 多个定时任务类,但是如果 `export default` 了,则只会读取 `default` 的类。
:::tip
推荐使用 `CommonSchedule` 接口来规范你的计划任务类。
:::

### 注入日志对象

Expand Down
8 changes: 6 additions & 2 deletions packages/midway-decorator/src/common/schedule.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { saveClassMetaData, saveModule, scope, ScopeEnum } from 'injection';
import { SCHEDULE_KEY } from '../constant';

export interface SchedueOpts {
export interface CommonSchedule {
exec(ctx?);
}

export interface ScheduleOpts {
type: string;
cron?: string;
interval?: number | string;
Expand All @@ -14,7 +18,7 @@ export interface SchedueOpts {
};
}

export function schedule(scheduleOpts: SchedueOpts | string) {
export function schedule(scheduleOpts: ScheduleOpts | string) {
return function (target: any): void {
saveModule(SCHEDULE_KEY, target);
saveClassMetaData(SCHEDULE_KEY, scheduleOpts, target);
Expand Down
1 change: 0 additions & 1 deletion packages/midway-decorator/src/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@ export const FUNC_KEY = 'func';
export const HANDLER_KEY = 'handler';

// web
export const MIDDLEWARE_KEY = 'middleware';
export const CONTROLLER_KEY = 'controller';
export const WEB_ROUTER_KEY = 'web_router';
1 change: 1 addition & 0 deletions packages/midway-mock/src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface MidwayApplicationOptions extends MockOption {
plugins?: any;
container?: any;
typescript?: boolean;
worker?: number;
}

export interface MidwayMockApplication extends MockApplication {
Expand Down
47 changes: 47 additions & 0 deletions packages/midway-schedule/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,53 @@ this is a sub package for midway.

Document: [https://midwayjs.org/midway](https://midwayjs.org/midway)

## Config

Midway has enabled this plugin by default.

```js
// plugin.ts(js)
exports.schedulePlus = {
enable: true,
package: 'midway-schedule'
}
```

## Detail

The schedule of midway is based on [egg schedule](https://eggjs.org/en/basics/schedule.html), and provide more typescript and decorator support. The task can store in any file like `src/schedule`, it can be configured the properties and specify jobs. For example:

```typescript
// src/schedule/hello.ts
import { provide, schedule, CommonSchedule } from 'midway';

@provide()
@schedule({
interval: 2333, // 2.333s interval
type: 'worker', // only run in certain worker
})
export class HelloCron implements CommonSchedule {
// The detail job while times up
async exec(ctx) {
ctx.logger.info(process.pid, 'hello');
}
}
```

It is recommended to use `CommonSchedule` interface to standardize your schedule class.

## About run schedule

Egg provides the app.runSchedule method to test the scheduled task. The parameters of the midway are different. The format of the midway parameter is `ioc id#className`.

Take the example of the `HelloCron` class above.

```js
app.runSchedule('helloCron#HelloCron');
```

Please see our test case for more detail.

## License

[MIT]((http://github.com/midwayjs/midway/blob/master/LICENSE))
47 changes: 46 additions & 1 deletion packages/midway-schedule/agent.ts
Original file line number Diff line number Diff line change
@@ -1 +1,46 @@
module.exports = require('egg-schedule/agent');
import { ScheduleOpts, SCHEDULE_KEY } from '@midwayjs/decorator';
import { getClassMetaData, listModule, TagClsMetadata, TAGGED_CLS } from 'injection';
import 'reflect-metadata';

export = (agent) => {

// ugly!! just support all and worker strategy
class AllStrategy extends agent['TimerScheduleStrategy'] {
handler() {
this.sendAll();
}
}

class WorkerStrategy extends agent['TimerScheduleStrategy'] {
handler() {
this.sendOne();
}
}

const strategyMap = new Map();
strategyMap.set('worker', WorkerStrategy);
strategyMap.set('all', AllStrategy);

agent.messenger.once('egg-ready', () => {
const schedules: any[] = listModule(SCHEDULE_KEY);
for (const scheduleModule of schedules) {
const metaData = Reflect.getMetadata(TAGGED_CLS, scheduleModule) as TagClsMetadata;
const opts: ScheduleOpts = getClassMetaData(SCHEDULE_KEY, scheduleModule);
const type = opts.type;
if (opts.disable) {
continue;
}
const key = metaData.id + '#' + scheduleModule.name;
const Strategy = strategyMap.get(type);
if (!Strategy) {
const err = new Error(`schedule type [${type}] is not defined`);
err.name = 'MidwayScheduleError';
throw err;
}

const instance = new Strategy(opts, agent, key);
instance.start();
}
});

};
133 changes: 29 additions & 104 deletions packages/midway-schedule/app.ts
Original file line number Diff line number Diff line change
@@ -1,110 +1,35 @@
'use strict';

import * as qs from 'querystring';
import * as path from 'path';
import loadSchedule from './lib/load_schedule';
import * as fs from 'fs';

const loadEggSchedule = require('egg-schedule/lib/load_schedule');

module.exports = (app) => {
// don't redirect scheduleLogger
app.loggers.scheduleLogger.unredirect('error');

// 'app/schedule' load egg-schedule (spec for egg-logxx rotate)
const schedules = loadEggSchedule(app);
// 'lib/schedule' load midway-schedule (class only with decorator support)
loadSchedule(app);

// for test purpose
app.runSchedule = (schedulePath, key = 'default') => {
if (!path.isAbsolute(schedulePath)) {
schedulePath = path.join(
app.config.baseDir,
'app/schedule',
schedulePath,
);
if (!fs.existsSync(schedulePath)) {
schedulePath = path.join(
app.config.baseDir,
'lib/schedule',
schedulePath,
import { ScheduleOpts, SCHEDULE_KEY } from '@midwayjs/decorator';
import { getClassMetaData, listModule, TagClsMetadata, TAGGED_CLS } from 'injection';
import * as is from 'is-type-of';
import 'reflect-metadata';

export = (app) => {
const schedules: any[] = listModule(SCHEDULE_KEY);
for (const scheduleModule of schedules) {
const metaData = Reflect.getMetadata(TAGGED_CLS, scheduleModule) as TagClsMetadata;
if (metaData) {
const key = metaData.id + '#' + scheduleModule.name;
const opts: ScheduleOpts = getClassMetaData(SCHEDULE_KEY, scheduleModule);
const task = async (ctx, data) => {
const ins = await ctx.requestContext.getAsync(scheduleModule);
ins.exec = app.toAsyncFunction(ins.exec);
return ins.exec(ctx, data);
};

const env = app.config.env;
const envList = opts.env;
if (is.array(envList) && !envList.includes(env)) {
app.coreLogger.info(
`[midway-schedule]: ignore schedule ${key} due to \`schedule.env\` not match`,
);
return;
}
app.schedules[key] = {
schedule: opts,
task,
key,
};
}
schedulePath = require.resolve(schedulePath);
let schedule;

try {
schedule = schedules[schedulePath] || schedules[schedulePath + '#' + key];
if (!schedule) {
throw new Error(`Cannot find schedule ${schedulePath}`);
}
} catch (err) {
err.message = `[egg-schedule] ${err.message}`;
return Promise.reject(err);
}

// run with anonymous context
const ctx = app.createAnonymousContext({
method: 'SCHEDULE',
url: `/__schedule?path=${schedulePath}&${qs.stringify(
schedule.schedule,
)}`,
});

return schedule.task(ctx);
};

// log schedule list
for (const s in schedules) {
const schedule = schedules[s];
if (!schedule.schedule.disable)
app.coreLogger.info('[egg-schedule]: register schedule %s', schedule.key);
}

// register schedule event
app.messenger.on('egg-schedule', (data) => {
const id = data.id;
const key = data.key;
const schedule = schedules[key];
const logger = app.loggers.scheduleLogger;
logger.info(`[${id}] ${key} task received by app`);

if (!schedule) {
logger.warn(`[${id}] ${key} unknown task`);
return;
}
/* istanbul ignore next */
if (schedule.schedule.disable) return;

// run with anonymous context
const ctx = app.createAnonymousContext({
method: 'SCHEDULE',
url: `/__schedule?path=${key}&${qs.stringify(schedule.schedule)}`,
});

const start = Date.now();
const task = schedule.task;
logger.info(`[${id}] ${key} executing by app`);
// execute
task(ctx, ...data.args)
.then(() => true) // succeed
.catch((err) => {
logger.error(`[${id}] ${key} execute error.`, err);
err.message = `[egg-schedule] ${key} execute error. ${err.message}`;
app.logger.error(err);
return false; // failed
})
.then((success) => {
const rt = Date.now() - start;
const status = success ? 'succeed' : 'failed';
ctx.coreLogger.info(
`[egg-schedule] ${key} execute ${status}, used ${rt}ms`,
);
logger[success ? 'info' : 'error'](
`[${id}] ${key} execute ${status}, used ${rt}ms`,
);
});
});
};
41 changes: 0 additions & 41 deletions packages/midway-schedule/app/extend/agent.ts

This file was deleted.

Loading

0 comments on commit e76abb4

Please sign in to comment.