xxl-job源码分析(四):执行器执行任务和结果回调

3,104 阅读5分钟

xxl-job架构

image.png

一.概要

在上一章中,我们介绍了xxl-admin是如果定时查询需要执行的任务,找到该任务后,会封装任务参数,通过配置的策略,从启动的执行器中选择一个具体的url,调用执行器的run方法,进行触发任务,这一篇文章,我们就来详细进行源码分析

二.执行器执行任务

在之前文章中# xxl-job源码分析(二):业务接入方启动过程 中,业务方启动时候,会启动一个netty项目,会在netty中添加四个handler,如图所示

image.png 这个类就是处理http请求的handle,当有请求进来的时候,就回触发channelRead方法,对netty不了解的话,可以看我之前写的netty系列文章 netty源码解析

EmbedHttpServerHandler

进入这个handler

protected void channelRead0(final ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
    // request parse
    //final byte[] requestBytes = ByteBufUtil.getBytes(msg.content());    // byteBuf.toString(io.netty.util.CharsetUtil.UTF_8);
    // TODO 1.获取请求参数
    String requestData = msg.content().toString(CharsetUtil.UTF_8);
    //TODO 2.获取请求url =/run
    String uri = msg.uri();
    //TODO 3.获取请求方式 GET or POST
    HttpMethod httpMethod = msg.method();
    boolean keepAlive = HttpUtil.isKeepAlive(msg);
    String accessTokenReq = msg.headers().get(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN);

    // 开启线程执行任务
    bizThreadPool.execute(new Runnable() {
        @Override
        public void run() {
            // TODO 4.执行任务
            Object responseObj = process(httpMethod, uri, requestData, accessTokenReq);

            // to json
            String responseJson = GsonTool.toJson(responseObj);

            //TODO 5.返回结果(直接返回成功,任务会继续异步执行)
            writeResponse(ctx, keepAlive, responseJson);
        }
    });
}

进入 Object responseObj = process(httpMethod, uri, requestData, accessTokenReq);

image.png

public ReturnT<String> run(TriggerParam triggerParam) {
    // load old:jobHandler + jobThread TODO 查询这个任务的线程,第一次执行任务,是没有这个任务的
    JobThread jobThread = XxlJobExecutor.loadJobThread(triggerParam.getJobId());
    IJobHandler jobHandler = jobThread!=null?jobThread.getHandler():null;
    String removeOldReason = null;

    // valid:jobHandler + jobThread
    GlueTypeEnum glueTypeEnum = GlueTypeEnum.match(triggerParam.getGlueType());
    if (GlueTypeEnum.BEAN == glueTypeEnum) {

        // todo 创建 jobhandler,备注,执行器在启动的时候,会扫描@xxlJob注解修饰的方法,注册到map中,这里直接取出来,IJobHandler就是对jdk的method对象的封装
        IJobHandler newJobHandler = XxlJobExecutor.loadJobHandler(triggerParam.getExecutorHandler());

        // valid old jobThread
        if (jobThread!=null && jobHandler != newJobHandler) {
            // change handler, need kill old thread
            removeOldReason = "change jobhandler or glue type, and terminate the old job thread.";

            jobThread = null;
            jobHandler = null;
        }

        // valid handler
        if (jobHandler == null) {
            jobHandler = newJobHandler;
            if (jobHandler == null) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, "job handler [" + triggerParam.getExecutorHandler() + "] not found.");
            }
        }

    } else if (GlueTypeEnum.GLUE_GROOVY == glueTypeEnum) {

        // valid old jobThread
        if (jobThread != null &&
                !(jobThread.getHandler() instanceof GlueJobHandler
                    && ((GlueJobHandler) jobThread.getHandler()).getGlueUpdatetime()==triggerParam.getGlueUpdatetime() )) {
            // change handler or gluesource updated, need kill old thread
            removeOldReason = "change job source or glue type, and terminate the old job thread.";

            jobThread = null;
            jobHandler = null;
        }

        // valid handler
        if (jobHandler == null) {
            try {
                IJobHandler originJobHandler = GlueFactory.getInstance().loadNewInstance(triggerParam.getGlueSource());
                jobHandler = new GlueJobHandler(originJobHandler, triggerParam.getGlueUpdatetime());
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
                return new ReturnT<String>(ReturnT.FAIL_CODE, e.getMessage());
            }
        }
    }  

    // executor block strategy
    if (jobThread != null) {
        ExecutorBlockStrategyEnum blockStrategy = ExecutorBlockStrategyEnum.match(triggerParam.getExecutorBlockStrategy(), null);
        if (ExecutorBlockStrategyEnum.DISCARD_LATER == blockStrategy) {
            // discard when running
            if (jobThread.isRunningOrHasQueue()) {
                return new ReturnT<String>(ReturnT.FAIL_CODE, "block strategy effect:"+ExecutorBlockStrategyEnum.DISCARD_LATER.getTitle());
            }
        } else if (ExecutorBlockStrategyEnum.COVER_EARLY == blockStrategy) {
            // kill running jobThread
            if (jobThread.isRunningOrHasQueue()) {
                removeOldReason = "block strategy effect:" + ExecutorBlockStrategyEnum.COVER_EARLY.getTitle();

                jobThread = null;
            }
        } else {
            // just queue trigger
        }
    }

    // TODO jobThread 为null的话,就创建一个,并执行线程,是一个死循环,从 triggerQueue中读取参数,并执行
    if (jobThread == null) {
        jobThread = XxlJobExecutor.registJobThread(triggerParam.getJobId(), jobHandler, removeOldReason);
    }

    // TODO 把参数放在任务队列线程中的 triggerQueue 中
    ReturnT<String> pushResult = jobThread.pushTriggerQueue(triggerParam);
    return pushResult;
}

上面这一段业务逻辑比较长,核心是下面几个不走

  • 1.找个这个任务对应的JobThread,如果没有,下面会创建
  • 2.找个这个JobThread对应的jobHander,如果没有,就从jobHandlerRepository中获取,备注:jobHandlerRepository就是执行器在启动的时候,扫描@xxlJob注解后,放在jobHandlerRepository中,第二篇文章说过了
  • 3.jobThread线程会一直死循环, 从triggerQueue中获取参数,执行我们都任务代码
jobThread = XxlJobExecutor.registJobThread(triggerParam.getJobId(), jobHandler, removeOldReason);
  • 4.把本次触发的参数放入jobThread的triggerQueue中,让步骤[3]去执行
ReturnT<String> pushResult = jobThread.pushTriggerQueue(triggerParam);

三.jobThread的执行逻辑

这一块代码较长,如下:

public void run() {

       // 如果注解中有init方法,就先执行init方法
       try {
      handler.init();
   } catch (Throwable e) {
          logger.error(e.getMessage(), e);
   }

   // execute
   while(!toStop){
      running = false;
      idleTimes++;

           TriggerParam triggerParam = null;
           try {
         // todo 从triggerQueue队列中取值,最多阻塞3s
         triggerParam = triggerQueue.poll(3L, TimeUnit.SECONDS);
         if (triggerParam!=null) {
            running = true;
            idleTimes = 0;
            triggerLogIdSet.remove(triggerParam.getLogId());

            // 记录日志相关,不重要
            String logFileName = XxlJobFileAppender.makeLogFileName(new Date(triggerParam.getLogDateTime()), triggerParam.getLogId());
            XxlJobContext xxlJobContext = new XxlJobContext(
                  triggerParam.getJobId(),
                  triggerParam.getExecutorParams(),
                  logFileName,
                  triggerParam.getBroadcastIndex(),
                  triggerParam.getBroadcastTotal());

            // init job context
            XxlJobContext.setXxlJobContext(xxlJobContext);

            // execute
            XxlJobHelper.log("<br>----------- xxl-job job execute start -----------<br>----------- Param:" + xxlJobContext.getJobParam());
            //TODO 如果任务配置的时候,有执行时间限制的话,会使用callback方式执行
            if (triggerParam.getExecutorTimeout() > 0) {
               // limit timeout
               Thread futureThread = null;
               try {
                  FutureTask<Boolean> futureTask = new FutureTask<Boolean>(new Callable<Boolean>() {
                     @Override
                     public Boolean call() throws Exception {

                        // init job context
                        XxlJobContext.setXxlJobContext(xxlJobContext);

                        handler.execute();
                        return true;
                     }
                  });
                  futureThread = new Thread(futureTask);
                  futureThread.start();

                  Boolean tempResult = futureTask.get(triggerParam.getExecutorTimeout(), TimeUnit.SECONDS);
               } catch (TimeoutException e) {

                  XxlJobHelper.log("<br>----------- xxl-job job execute timeout");
                  XxlJobHelper.log(e);

                  // handle result
                  XxlJobHelper.handleTimeout("job execute timeout ");
               } finally {
                  futureThread.interrupt();
               }
            } else {
               // TODO 调用我们都@xxlJob修饰的任务方法
               handler.execute();
            }

            // valid execute handle data
            if (XxlJobContext.getXxlJobContext().getHandleCode() <= 0) {
               XxlJobHelper.handleFail("job handle result lost.");
            } else {
               String tempHandleMsg = XxlJobContext.getXxlJobContext().getHandleMsg();
               tempHandleMsg = (tempHandleMsg!=null&&tempHandleMsg.length()>50000)
                     ?tempHandleMsg.substring(0, 50000).concat("...")
                     :tempHandleMsg;
               XxlJobContext.getXxlJobContext().setHandleMsg(tempHandleMsg);
            }
            XxlJobHelper.log("<br>----------- xxl-job job execute end(finish) -----------<br>----------- Result: handleCode="
                  + XxlJobContext.getXxlJobContext().getHandleCode()
                  + ", handleMsg = "
                  + XxlJobContext.getXxlJobContext().getHandleMsg()
            );

         } else {
            if (idleTimes > 30) {
               if(triggerQueue.size() == 0) { // avoid concurrent trigger causes jobId-lost
                  XxlJobExecutor.removeJobThread(jobId, "excutor idel times over limit.");
               }
            }
         }
      } catch (Throwable e) {
         if (toStop) {
            XxlJobHelper.log("<br>----------- JobThread toStop, stopReason:" + stopReason);
         }

         // TODO 失败的话,日志中记录失败信息
         StringWriter stringWriter = new StringWriter();
         e.printStackTrace(new PrintWriter(stringWriter));
         String errorMsg = stringWriter.toString();
      
         XxlJobHelper.handleFail(errorMsg);

         XxlJobHelper.log("<br>----------- JobThread Exception:" + errorMsg + "<br>----------- xxl-job job execute end(error) -----------");
      } finally {
               if(triggerParam != null) {
                   // callback handler info
                   if (!toStop) {
                       // TODO 把结果放入callBackQueue 队列中,用于其他线程告诉xxl-admin执行的结果
                       TriggerCallbackThread.pushCallBack(new HandleCallbackParam(
                              triggerParam.getLogId(),
                     triggerParam.getLogDateTime(),
                     XxlJobContext.getXxlJobContext().getHandleCode(),
                     XxlJobContext.getXxlJobContext().getHandleMsg() )
               );
                   } else {
                       // is killed
                       TriggerCallbackThread.pushCallBack(new HandleCallbackParam(
                              triggerParam.getLogId(),
                     triggerParam.getLogDateTime(),
                     XxlJobContext.HANDLE_CODE_FAIL,
                     stopReason + " [job running, killed]" )
               );
                   }
               }
           }
       }

   // callback trigger request in queue
   while(triggerQueue !=null && triggerQueue.size()>0){
      TriggerParam triggerParam = triggerQueue.poll();
      if (triggerParam!=null) {
         // is killed
         TriggerCallbackThread.pushCallBack(new HandleCallbackParam(
               triggerParam.getLogId(),
               triggerParam.getLogDateTime(),
               XxlJobContext.HANDLE_CODE_FAIL,
               stopReason + " [job not executed, in the job queue, killed.]")
         );
      }
   }

   // TODO 执行任务的destroy方法
   try {
      handler.destroy();
   } catch (Throwable e) {
      logger.error(e.getMessage(), e);
   }

   logger.info(">>>>>>>>>>> xxl-job JobThread stoped, hashCode:{}", Thread.currentThread());
}

核心逻辑如下:

  • 1.如果注解中有init方法,就先执行init方法
  • 2.从triggerQueue队列中取出任务执行的参数,最多阻塞3s
  • 3.记录日志
  • 4.执行任务
  • 4-1.如果任务执行失败,就记录失败信息到日志中
  • 4-2 最后把执行结果放在 TriggerCallbackThread线程的callBackQueue队列中,TriggerCallbackThread线程回回调这些结果
  • 5.如果任务停止的话,就调用任务的destroy方法

四.jobThrad线程什么时候会被终止

因为每一个任务执行的时候,都会对应一个jobThread,如果这个任务不是周期性的执行,而是只执行一次的话,那么这个线程就需要在一定的时机,关闭线程,释放资源

4.1 循环30次,这个线程没有任务,关闭

在上一章中,执行任务是一个死循环,会记录循环的次数

image.png

4.2 项目关闭的时候,会关闭所有的任务的线程池

image.png

4.3 调速器主动发送kill请求的时候

image.png

image.png

五,结果回填

在第四章中,讲解了把执行结果放在 TriggerCallbackThread线程的callBackQueue队列中,TriggerCallbackThread线程回回调这些结果,大家可以去看一下,也比较简单,# xxl-job源码分析(二):业务接入方启动过程的 [ 3.再来看start方法中的 TriggerCallbackThread线程]中也写了