Sailboat执行器的编写和日志的生成
在sailboat/executor中新建名为actuator的Python文件,并定义执行者方法。对应的代码如下:
将其引入到TimerHandler所在的文件,并传给APScheduler的add_job() 方法。代码改动如下:
保存改动后,再次用Postman工具测试。此时服务端返回的信息如下:
并且运行Sailboat的控制台输出了performer() 方法中打印的内容:
这说明调度接口能够正常工作了。现在我们要做的是完善执行器的代码,让Sailboat能够定时调度指定的EGG文件。
执行器要完成的工作并不多,大体流程如下:
- ·解包EGG文件。
- ·执行文件中指定的方法。
- ·记录执行时间。
- ·构造执行信息并存入数据库。
项目的解包和执行可以使用多进程或者多线程执行,最大化地发挥服务器的能力。考虑到项目日志的获取和生成,这里用Python中的内置函数subprocess.Popen() 启动执行器,这样就可以获取到被执行项目在运行过程中的输出和异常信息,这正是被执行项目日志生成的基础。
Sailboat的执行器代码便不难设计:
- ·执行器启动时接收传入的参数。
- ·构造EGG文件路径。
- ·解包EGG文件。
- ·调用EGG文件中指定的方法。
这里我们选择将EGG文件拷贝到临时区执行,执行完毕后需要删除临时区中对应的文件。熟悉Python上下文管理器的读者很快就得出了执行器的整体框架,执行器sailboat/executor/boat.py文件的完整代码
import os
import sys
from importlib import import_module
from components.storage import FileStorages
storages = FileStorages()
class Helmsman:
"""为文件导入和执行创造条件的上下文管理器"""
def __init__(self, project, version):
self.project = project
self.version = version
self.storage = storages
def __enter__(self):
"""上文"""
# 将文件拷贝到临时区
target = self.storage.copy_to_temporary(self.project, self.version)
self.temp_file = target
if target:
# 将文件路径添加到 sys.path
sys.path.insert(0, target)
def __exit__(self, exc_type, exc_val, exc_tb):
"""下文"""
if os.path.exists(self.temp_file):
# 清理临时区中对应的文件
os.remove(self.temp_file)
def main(project, version):
hemsman = Helmsman(project, version)
with hemsman:
# 从指定的文件中导入模块并调用指定方法
spider = import_module("sail")
spider.main()
if __name__ == '__main__':
project, version = sys.argv[-2], sys.argv[-1]
main(project, version)
执行器中代码的执行过程和上面设计的一样。需要注意的是,解包运行时导入的是名为sail的模块,并调用指定的main() 方法。也就是说,在打包Python项目时,项目必须符合如下结构:
入口函数main() 必须写在__init__.py文件中。这样一来,只要符合这个结构的Python项目都可以部署到Sailboat上进行管理和调度。执行器的代码确定后,完善执行者performer() 的代码。上面提到了Popen() ,依照Python文档中相应的介绍,执行者的代码顺势而成:
import os
import sys
import logging
import subprocess
from datetime import datetime
from uuid import uuid4
from settings import LOGPATH, RIDEPATH
from connect import databases
def delta_format(delta):
"""时间差格式化
将时间差格式转换为时分秒
"""
seconds = delta.seconds
minutes, second = divmod(seconds, 60)
hour, minute = divmod(minutes, 60)
return "%s:%s:%s" % (hour, minute, second)
def time_format(start_time, end_time):
"""时间格式化
将时间格式转换为年月日时分秒
"""
duration = delta_format(end_time - start_time)
start = start_time.strftime("%Y-%m-%d %H:%M:%S")
end = end_time.strftime("%Y-%m-%d %H:%M:%S")
return start, end, duration
def performer(*args, **kwargs):
"""执行者
获取 stdout stderr
生成日记文件名
将日记写入文件
将执行信息,执行结果,以及启动时间,结果时间和运行时长存入数据库
"""
job = str(uuid4())
# 接受传入的参数
project, version, mode, rule, jid, idn, username = args
# 开启子进程调用执行器
instructions = ['-m', RIDEPATH, project, version]
# 记录启动时间
start = datetime.now()
sub = subprocess.Popen(instructions, executable=sys.executable, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# 获取输出和异常信息
stdout, stderr = sub.communicate()
# 记录结束时间
end = datetime.now()
out, err = "", ""
try:
out = stdout.decode("utf8")
err = stderr.decode("utf8")
except Exception as e:
logging.warning(e)
# 格式化起止时间并计算运行时长
start, end, duration = time_format(start, end)
# 生成日记
if not os.path.exists(LOGPATH):
os.makedirs(LOGPATH)
room = os.path.join(LOGPATH, project)
if not os.path.exists(room):
os.makedirs(room)
log = os.path.join(room, "%s.log" % job)
with open(log, 'w') as file:
# 将子进程输出写入日记文件
file.write(out)
file.write(err)
# 构造执行记录并存储到数据库中
message = {
"project": project,
"version": version,
"mode": mode,
"job": job,
"start": start,
"end": end,
"duration": duration,
"jid": jid,
"idn": idn,
"username": username,
"create": datetime.now()
}
databases.record.insert_one(message).inserted_id
这里用到的LOGPATH和RIDEPATH是日志存放路径和执行器路径,它们定义在sailboat/settings.py文件中:
执行者通过subprocess.Popen() 方法启动执行器的boat.py文件,在启动前后分别记录下当前时间,两次记录的时间就是项目启动时间和项目运行结束时间,它们的差值就是项目运行时长。
运行过程中产生的输出和异常信息可以通过communicate() 方法获取,接着将它们组合起来写入到文本文件中,这便是项目运行日志。
Sailboat定时调度功能的实现
准备好执行者和执行器后,回到TimerHandler中,执行者编写的参数接收语句正是预留给TimerHandler的,这样我们就可以将项目信息和用户信息传递给执行者,待执行器运行结束后,信息就会存储到MongoDB中。TimerHandler的代码改动如下:
保存改动后用Postman进行新一轮的测试,客户端请求信息如下:
服务端返回的信息与上次相似,没有问题。Sailboat目录下多出了与日志相关的文件,日志目录结构如下:
打开任意一个日志文件,其内容为:
200
这里记录的200正是EGG文件中sail项目的运行输出。