其实本文不知道算不算一个知识点分享,过程很美妙,但结果很失败。咱们在利用Optaplanner的Real-Time planning(实时规则)功能,设计实时在线规划服务时,遇到一个属于Optaplanner7.8.0.Final版本的Bug。在实现实时在线规划服务的过程当中,我作过不少尝试。由于须要实时在线的服务,所以,须要设计多线程并发为外界请求提供响应,须要实现消息队列来管理并发请求的时序等问题。这些Java方面的并发处理,咱们暂时不详述,这方面的牛的人太多了,我只是新手,站在别人的肩膀上实现的代码而已。在本文我着重介绍一下,我在尝试使用Optaplanner的Real-Time Planning功能时遇到的问题,最终确认问题出自Optaplanner引擎自身, 并经过JIRA向Optaplanner 团队提交issue过程。web
先看看正常状况下,咱们对Optaplanner的应用场景。平时咱们使用Optaplanner时,不外乎如下几个, 构建Problem对象 + 构建Solver对象-> 启动引擎 -> 执行规划 -> 结束规划 -> 得到方案-> 获取结果方案,以下图。服务器
这种应用模式下,引擎处于一个非实时状态,只是一个调用 -> 获取规划结果的简单交互过程。微信
可是有些对规划具的时间性要求较高,或在时间序列上,对规划的结果具备必定的延续性要求的状况下,这种规划方式是知足不了要求的。例若有些实时调度的场景;要求每一个新的solution与上一个solution须要具备延续性,不可能每次给出的solution存在过大的差别,若产生过大的差别,这些规划出来的方案对于执行机构来讲,是不可能按计划执行的。例如车辆调度系统(见下图),每隔一个时间段,就须要刷新一下车辆状况和环境状况,不可能每次刷新出来的调度方案跟前一次存在千差万别。每一次产生的方案,它必须尽最大程度上与上一次保持相近。网络
另一个要求是实时性,若是按传的规划步骤,对于实时性有要求,或响应速度较高的场景,例如:车间做业的实时调度系统,可能每隔离10分钟就须要刷新一次计划,此时实时规则的做用就反映出来了。以下动图:多线程
Real-time planning, 顾名思义就是实时规划,它与传统的规划步骤区别在于,它并无一个结束并退出规划的动做,面是一旦引擎启动,它将以守护进程的形式一直处于运行状态,而没有返回;当它知足规划结束条件时(例如找到符合条件的方案,或到达规划时限),会进入值守状态,不占用CPU资源。待激发事件对它发出从新启动的指令。所以,它的步骤是: [构建Problem对象] + [构建Solver对象] -> 启动引擎 -> 规划 -> 经过BestSolutionChange事件输出规则方案 -> 休眠 -> 接到重启指令 -> 规则(重重上述步骤),以下图:并发
原来Optaplanner还有这种神操做,那么它的做用将进一步大增了,幻想一下你们看科幻或战争电影时,那里的指挥中心必然有一个大屏幕,上面显示了实时的战况或各方资源的部署状况,若是这些部署是须要经过规划来辅助实现的话,Optaplanner是否是能够做为后台超级计算机上不停运算规划的控制中枢系统呢?不过好像想多了。没那么神,作一下实时做业调度仍是能够的。下面就看看咱们的项目是如何考虑应用Real-time planning的。ide
关于Real-Time Planning的具体开发步骤没办法在这里详述,在本系列的日后文章中,老农将会有一篇专门的文章介绍。它的基本步骤以下图。工具
这里提供一下最重要的三个代码块,对应的场景是,当一个新的任务(Task)须要被添加进引擎的Problem中参与规则时,应该如何添加,添加完成以后,如何得到规划的结果。这三个代码块的功能分别是bestSolutionChanged事件处理程序,调用引擎Solver对象提交变动请求,和实现ProblemFactChange接口的实现,用于实现变动正在规划的Planning Entity.单元测试
bestSolutionChanged事件处理程序测试
1 // solver是一个Solver对象,引擎入口
2 solver.addEventListener(new SolverEventListener<TaskAssignmentSolution>() { 3 public void bestSolutionChanged(BestSolutionChangedEvent<TaskAssignmentSolution> event) { 4 if(solver.isEveryProblemFactChangeProcessed()) { 5 // TODO: 获取规划结果 6 } 7 } 8 });
调用引擎Solver对象提交变动
1 DeleteTaskProblemFactChange taskProblemChange = new DeleteTaskProblemFactChange(task); 2 if (solver.isSolving()) { 3 solver.addProblemFactChange(taskProblemChange); 4 } else { 5 taskProblemChange.doChange(scoreDirector); 6 scoreDirector.calculateScore(); 7 }
ProblemFactChange接口的实现
1 /** 2 * 添加任务到Workingsolution 3 * @author ZhangKent 4 * 5 */ 6 public class AddTaskProblemChange extends AbstractPersistable implements ProblemFactChange<TaskAssignmentSolution>{ 7 private final Task task; 8 9 public AddTaskProblemChange(Task task){ 10 this.task = task; 11 } 12 13 @Override 14 public void doChange(ScoreDirector<TaskAssignmentSolution> scoreDirector) { 15 16 TaskAssignmentSolution taskAssignmentSolution = scoreDirector.getWorkingSolution(); 17 18 scoreDirector.beforeEntityAdded(this.task); 19 taskAssignmentSolution.getTaskList().add(this.task); 20 scoreDirector.afterEntityAdded(this.task); 21 scoreDirector.triggerVariableListeners(); 22 } 23 }
场景要求
咱们的项目其实挺符合实时做业的要求的,虽然咱们也没有要求达到分钟级,或秒级的响应;可是若是可以每隔离10分钟,经过实时规划的模式刷新一次计划,仍是更能帮助生产调度人员更准确掌握生产状况的。事实上,咱们对新的计划刷新条件,并非按固定的时间间隔来进行,而是以触发事件的方式对进行变动规划的。
即当一个新任务产生了,或一个已计划好的任务被生产完成了,或一个已计划好的任务没法按时执行生产做业而产生计划与实际状况存在差别时,或一个机台出现计划之外的停机等诸如此类对计划足以产生影响的事件,都将会做为触发从新规则的条件。所以,我将引擎程序作成Springboot程序,部署到服务器端,并将程序设计成多线程并发的模式,主线程负责侦听Springboot接收到的WebAPI请求,当接收到请求后,就从线程池中启用一个线程对请求进行处理,这些处理是更新规划的请求,并把传送过来的Planning Enitty, Problem Fact等信息按要求进行处理,并放入队列中。全部请求产生的从新规划信息,经过队列依次被送入引擎处理。当有新的solution产生时,将它输出指定位置,并通知客户端前往获取。
系统的构件结构以下图。
古语有云,理想很丰满,现实很骨感。上述的设计对于Optaplanner的使用领域来讲,是比较先进的(起码在国内还没据说过有人这样用法)。对业务而言也是很是符合要求的。可是我对上述全部美妙的构想完成了设计,并实现了代码,并经过Springboot运行起来以后。程序确实如我意图那样运行起来了!启动引擎 -> 开始规则 -> 找到更佳方案 -> 输出方案 -> 知足中止条件 -> 引擎进入守值状态. 好了,我就经过http发出一个删除Planning Entity的请求。Springboot的Contoller成功接收,启动子线程处理数据,向引擎对象发送doChange请求,引擎检测到请求,分出一个线程(这个线程是引擎分出来处理我那个线程请求的)处理成功,并更新Problem对象中的Planning Entity列表;引擎继续运行。Duang~~~~引擎主线程居然抛出一个异常并中止了!提示那个被请求删除的Planning Entity未被加入Planning Entity的列表中!这下我蒙了。为何还会报出这个Planning Entity未被加进列表的错误?回想起Optaplanner的开发说明书里,关于Planning过程当中,每一个新的solution都是一个clone的状况,我坚信个人程序是遇到Race condition了,必定是个人程序考虑不周致使资源竞争。Optaplanner号称通过大量单元测试,压力测试,有良好的稳定性,不可能就这样被我把错误试出来的。但切切实实地抛出了这个异常,而我却没有任何办法。错误信息以下图,下图是我截取给Optaplanner团队的:
而后,我花了两天时间,对每个步骤进行调试分析,对每个solution的clone进行核对,我确实没办法从个人程序中找到任何头绪。因而我惟有求助于Geoffrey大神。经过邮件讨论组我给他留了个贴子。很快Geoffrey大神就回复了(这个得给个赞,比利时跟咱们的时区相差很多吧?每次提的问题,他都能及时回复)。回复见下图,这个回复令了心被泼了一大桶冷水。它居然确实多是一个bug! 固然也有多是程序产生了race condition. 可我都找了两天了,实在没办法,才想到找Optaplanner团队。而后我就把这个问题的重现步骤在Optaplanner项目的JIRA中提交了一个issue,不知道这算不算我给Optaplanner做出的一点点贡献呢,期待处理结果呀。
其实在这两天时间时,我并不只仅是检查我本身的代码是否出现资源竞争问题,我还Debug进了Optaplanner的源代码里(7.8.0.Final版),并找到了异常的具体来源。发现确确实实是在我提交了ProblemFactChanged请求后,引擎也进行了处理,但由于引擎在处理了请求后,在新的Solution的clone中,并无被成功更新,也就是新的Planning Entity并无进入新的solution clone中,而致使处理程序没法识别新的Planning Entity, 就出错了。
如今办法有两个,一个是等Optaplanner团队在JIRA上对我提交的issue进行处理,看是否是真的在Optaplanner中存在这么一个Bug. 另外一种办法是我打算将个人程序进一步简化,将它与Springboot分离,跟Optaplanner的事件程序同样,经过其它方法启动线程来尝试Real-Time Planning.
Optaplanner引擎程序被包装成一个Springboot程序,并设置为daemon模式(守卫进程),Springboot Application启动后,引擎执行程序被一个线程启动。主线程向外提供Restful webservice,当有Web请求到达时,就启动一个线程用于执行Optaplanner的ProblemFactChange对象中的doChange方法,对现有solution中的Planning Entity列表中的对象进行增删改操做;并触发VariableListeners. 引擎在处理这些调用时,会产生新的bestSolution,并触发BestSolutionChangedEvent事件,在事件处理方法中,将最新的Solution中的Planning Entity列表输出便可得到增删改Planning Entity后的最新solution了。
这又是一篇花费很多精力的东西,尽管最终没实现实时规划服务。
创做不易,欢迎转载,请标明出处。
本系列文章在公众号不定时连载,请关注公众号(让APS成为可能)及时接收,二维码:
如需了解更多关于Optaplanner的应用,请发电邮致:kentbill@gmail.com
或到讨论组发表你的意见:https://groups.google.com/forum/#!forum/optaplanner-cn
如有须要可添加本人微信(13631823503)或QQ(12977379)实时沟通,但因本人平常工做繁忙,经过微信,QQ等工具可能没法深刻沟通,较复杂的问题,建议以邮件或讨论组方式提出。(讨论组属于google邮件列表,国内网络可能较难访问,需自行解决)