如何使用 Play 实现预定的一次性流程! v1.2.4 和 Heroku Scheduler 插件

Posted

技术标签:

【中文标题】如何使用 Play 实现预定的一次性流程! v1.2.4 和 Heroku Scheduler 插件【英文标题】:How to implement a scheduled one-off process using Play! v1.2.4 and the Heroku Scheduler add-on 【发布时间】:2012-06-20 21:49:28 【问题描述】:

有一个example for doing this with Play! v2.0,但我正在尝试为 v1.2.4 做同样的事情

我尝试遵循相同的通用方法,即让Heroku Scheduler 使用初始化 Play 环境的类显式启动 JVM,然后在该环境中执行所需的任务。我的具体任务是使用 JPA 从数据库中检索一些数据,然后使用 Mailer 使用该数据生成电子邮件。最初的方法只是在初始化 Play 后直接执行逻辑,但我得到了一个异常,即“JPA 上下文未初始化”。

过程文件:

web: play run --http.port=$PORT $PLAY_OPTS
reports: java -Dapplication.path=./ -Dplay.id=staging -Dprecompiled=true -DlogLevel=INFO -cp "precompiled/java:lib/*:conf:.play/framework/play-1.2.4.jar:.play/framework/lib/*:modules/mailerPlus-0.1/lib/*:modules/fastergt-1.7/lib/*" jobs.Reports .

Reports.java:

public class Reports 

    private Reports() 
        super();
    

    public static void main(String[] args) 
        File root = new File(System.getProperty("application.path"));

        if (System.getProperty("precompiled", "false").equals("true")) 
            Play.usePrecompiled = true;
        

        try 
            Play.init(root, System.getProperty("play.id", ""));

            Mails.itjbSurveyReport();
         catch (Exception e) 
            Logger.error(e, "");
         finally 
            Play.stop();
        
    

日志:

2012-06-20T21:58:27+00:00 app[run.1]: play.exceptions.JPAException: The JPA context is not initialized. JPA Entity Manager automatically start when one or more classes annotated with the @javax.persistence.Entity annotation are found in the application.
2012-06-20T21:58:27+00:00 app[run.1]:   at play.db.jpa.JPA.get(JPA.java:22)
2012-06-20T21:58:27+00:00 app[run.1]:   at play.db.jpa.JPA.em(JPA.java:51)
2012-06-20T21:58:27+00:00 app[run.1]:   at play.db.jpa.JPQL.em(JPQL.java:18)
2012-06-20T21:58:27+00:00 app[run.1]:   at play.db.jpa.JPQL.count(JPQL.java:26)
2012-06-20T21:58:27+00:00 app[run.1]:   at models.User.count(User.java)
2012-06-20T21:58:27+00:00 app[run.1]:   at notifiers.Mails.itjbSurveyReport(Mails.java:246)
2012-06-20T21:58:27+00:00 app[run.1]:   at jobs.Reports.main(Reports.java:35)

然后决定将逻辑包装在“作业”中以确保 JPA 上下文已初始化,但随后我遇到了从 JPA 查询返回的对象的类加载问题:

Reports.java:

public class Reports extends Job 

    private Reports() 
        super();
    

    @Override
    public void doJob() throws Exception 
        Mails.itjbSurveyReport();
    

    public static void main(String[] args) 
        File root = new File(System.getProperty("application.path"));

        if (System.getProperty("precompiled", "false").equals("true")) 
            Play.usePrecompiled = true;
        

        try 
            Play.init(root, System.getProperty("play.id", ""));

            Reports reports = new Reports();
            reports.now().get();
         catch (Exception e) 
            Logger.error(e, "");
         finally 
            Play.stop();
        
    

日志:

2012-06-20T19:04:24+00:00 app[run.1]: ClassCastException occured : securesocial.provider.ProviderType cannot be cast to securesocial.provider.ProviderType
2012-06-20T19:04:24+00:00 app[run.1]: 
2012-06-20T19:04:24+00:00 app[run.1]: play.exceptions.JavaExecutionException: securesocial.provider.ProviderType cannot be cast to securesocial.provider.ProviderType
2012-06-20T19:04:24+00:00 app[run.1]:   at play.jobs.Job.call(Job.java:155)
2012-06-20T19:04:24+00:00 app[run.1]:   at play.jobs.Job$1.call(Job.java:66)
2012-06-20T19:04:24+00:00 app[run.1]:   at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:334)
2012-06-20T19:04:24+00:00 app[run.1]:   at java.util.concurrent.FutureTask.run(FutureTask.java:166)
2012-06-20T19:04:24+00:00 app[run.1]:   at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$101(ScheduledThreadPoolExecutor.java:165)
2012-06-20T19:04:24+00:00 app[run.1]:   at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:266)
2012-06-20T19:04:24+00:00 app[run.1]:   at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)
2012-06-20T19:04:24+00:00 app[run.1]:   at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
2012-06-20T19:04:24+00:00 app[run.1]:   at java.lang.Thread.run(Thread.java:636)
2012-06-20T19:04:24+00:00 app[run.1]: Caused by: java.lang.ClassCastException: securesocial.provider.ProviderType cannot be cast to securesocial.provider.ProviderType
2012-06-20T19:04:24+00:00 app[run.1]:   at notifiers.Mails.itjbSurveyReport(Mails.java:259)
2012-06-20T19:04:24+00:00 app[run.1]:   at jobs.Reports.doJob(Reports.java:36)
2012-06-20T19:04:24+00:00 app[run.1]:   at play.jobs.Job.doJobWithResult(Job.java:50)
2012-06-20T19:04:24+00:00 app[run.1]:   at play.jobs.Job.call(Job.java:146)
2012-06-20T19:04:24+00:00 app[run.1]:   ... 8 more

有趣的是,在上面提到的 v2.0 版本中,看起来本地类加载器被传递到 Play 环境中,这可能解决了这个问题。不确定如何在 v1.2.4 中做到这一点

不知道下一步该做什么......

【问题讨论】:

【参考方案1】:

这是一个工作示例应用:https://github.com/jamesward/play1-scheduled-job-demo

诀窍是从 Play 的类加载器(来自 HelloJob.java)加载 Job:

Class c = Play.classloader.loadClass("jobs.HelloJob");
Job s = (Job) c.newInstance();
s.run();

【讨论】:

是的,但重要的是,演员必须是你所拥有的“工作”,而不是我最初拥有的特定子类,否则会发生 ClassCastException。谢谢【参考方案2】:

为什么不直接添加

@Every("5min")
public class Reports extends Job 
  ...

我不确定你想要做什么。

如果你使用@Every 表示法,你不需要对 Heroku 做任何特别的事情。

【讨论】:

这样做的问题是,在集群中,每个节点都将执行相同的作业,并且需要某种集群范围的协调来确保一次只有一个节点正在执行该作业。 Heroku 调度程序的另一个优点是启动应用程序的另一个实例来执行作业,而不是影响处理 Web 请求的实例。我希望 Heroku 调度程序启动 Play 环境并运行一个类会是一个快速的胜利。 我认为您需要使用数据库管理作业,存储等待、启动、启动、失败和完成的状态。坦率地说,恕我直言,使用 PaaS 功能来管理这是一种危险的依赖关系。 无论如何,您最初的问题是尝试从 main 方法运行 play - 只是不要这样做 - 创建 JPA 类需要很多字节码魔法。而是将 play 作为 web 应用程序运行,并让您的“主要”调用特定路线来执行您的工作。所谓的调用是指对 URL 的 HTTP 请求。

以上是关于如何使用 Play 实现预定的一次性流程! v1.2.4 和 Heroku Scheduler 插件的主要内容,如果未能解决你的问题,请参考以下文章

使用 socket.io v1.3.x 将单个用户的请求发送给一组预定义的用户

如何在 Play Framework v1.2.7 中配置带有动态 id 属性的 POST 路由,该属性将加载 JPA 实体

在 Play Framework 2.x 中实现摘要式身份验证

使用 play framework v1.5 连接 db postgresql 时出错

Node.js 实现串行化流程控制

新的 Google Play 控制台 - 首次应用发布问题的托管发布,一次发布多个版本