package me.hao0.antares.store.service.impl; import com.github.kevinsawicki.http.HttpRequest; import com.google.common.base.Predicates; import com.google.common.base.Strings; import com.google.common.base.Throwables; import me.hao0.antares.common.balance.LoadBalance; import me.hao0.antares.common.balance.RandomLoadBalance; import me.hao0.antares.common.dto.JobDetail; import me.hao0.antares.common.exception.JobFindException; import me.hao0.antares.common.exception.JobStateTransferInvalidException; import me.hao0.antares.common.http.Http; import me.hao0.antares.common.http.HttpMethod; import me.hao0.antares.common.log.Logs; import me.hao0.antares.common.model.enums.JobState; import me.hao0.antares.common.retry.RetryException; import me.hao0.antares.common.retry.Retryer; import me.hao0.antares.common.retry.Retryers; import me.hao0.antares.common.util.CollectionUtil; import me.hao0.antares.common.util.Constants; import me.hao0.antares.common.util.Systems; import me.hao0.antares.store.dao.JobServerDao; import me.hao0.antares.store.exception.JobNotExistException; import me.hao0.antares.store.exception.JobServerException; import me.hao0.antares.store.service.ClusterService; import me.hao0.antares.store.service.JobService; import me.hao0.antares.store.service.ServerService; import me.hao0.antares.store.support.JobSupport; import me.hao0.antares.common.util.Response; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import static me.hao0.antares.store.util.ServerUris.*; /** * The service for calling server * Author: haolin * Email: haolin.h0@gmail.com */ @Service public class ServerServiceImpl implements ServerService { @Autowired private JobServerDao jobServerDao; @Autowired private JobService jobService; @Autowired private ClusterService clusterService; @Autowired private JobSupport jobSupport; private final Retryer<Boolean> serverRetryer = Retryers.get().newRetryer(Predicates.<Boolean>alwaysFalse(), 3); private final ExecutorService executor = Executors.newFixedThreadPool(Systems.cpuNum() + 1, new ThreadFactory() { private AtomicInteger index = new AtomicInteger(0); @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setName("SERVER-SERVICE-WORKER-" + index.incrementAndGet()); t.setDaemon(true); return t; } }); /** * Use random balance simply */ private LoadBalance<String> balancer = new RandomLoadBalance<>(); @Override public Response<Boolean> scheduleJob(Long jobId) { try { // get current server list Response<List<String>> listResp = clusterService.listSimpleServers(); if (!listResp.isSuccess()){ return Response.notOk(listResp.getErr()); } List<String> servers = listResp.getData(); if (CollectionUtil.isNullOrEmpty(servers)){ // no available server, don't need schedule Logs.warn("There are no available servers when schedule job(id={}).", jobId); return Response.notOk("server.no.available"); } String targetServer = balancer.balance(servers); return Response.ok(doScheduleJob(jobId, targetServer)); } catch (Exception e) { Logs.error("failed to schedule job(jobId={})", jobId); return Response.notOk("job.schedule.failed"); } } @Override public Response<Boolean> scheduleJobIfPossible(Long jobId) { try { String scheduleServer = jobServerDao.findServerByJobId(jobId); if (!Strings.isNullOrEmpty(scheduleServer)){ // re scheduling reloadJob(jobId); return Response.ok(true); } // get current server list Response<List<String>> listResp = clusterService.listSimpleServers(); if (!listResp.isSuccess()){ Logs.error("failed to list servers when schedule job(id={}) possible, cause: {}, but ignore", jobId, listResp.getErr()); return Response.ok(true); } List<String> servers = listResp.getData(); if (CollectionUtil.isNullOrEmpty(servers)){ // no available server, don't need schedule Logs.warn("There are no available server when schedule job(id={}) possible, but ignore.", jobId); return Response.ok(true); } return scheduleJob(jobId); } catch (Exception e) { Logs.error("failed to schedule job if possible(jobId={})", jobId); return Response.notOk("job.schedule.failed"); } } /** * Schedule the job to one of the servers * @param jobId the job id * @param servers the alive servers */ public Response<Boolean> scheduleJob(Long jobId, List<String> servers){ try { String targetServer = balancer.balance(servers); return Response.ok(doScheduleJob(jobId, targetServer)); } catch (Exception e) { Logs.error("failed to schedule job(jobId={}, servers={})", jobId, servers); return Response.notOk("job.schedule.failed"); } } /** * Schedule the jobs to the server * @param jobIds the job ids * @param servers the alive servers */ public Response<Boolean> scheduleJobs(List<Long> jobIds, final List<String> servers){ try { final CountDownLatch latch = new CountDownLatch(jobIds.size()); for(final Long jobId : jobIds){ executor.submit(new Runnable() { @Override public void run() { scheduleJob(jobId, servers); latch.countDown(); } }); } latch.await(); return Response.ok(true); } catch (InterruptedException e) { Logs.error("failed to count down latch await, cause: {}", Throwables.getStackTraceAsString(e)); return Response.notOk("job.schedule.failed"); } } private Boolean doScheduleJob(Long jobId, String targetServer) { String uri = JOB_SCHEDULE + "/" + jobId; try { return serverRetryer.call(new RetryableServerTask(targetServer, uri)); } catch (Exception e) { Logs.info("failed to schedule job(jobId={}, server={}), cause: {}", jobId, targetServer, Throwables.getStackTraceAsString(e)); return Boolean.FALSE; } } @Override public Response<Boolean> triggerJob(Long jobId) { try { checkJobState(jobId, JobState.WAITING, JobState.RUNNING); return Response.ok(doOperateJob(jobId, JOB_TRIGGER + "/" + jobId)); } catch (JobFindException e){ return Response.notOk("job.find.failed"); } catch (JobStateTransferInvalidException e){ return Response.notOk("job.state.operate.invalid"); } catch (JobNotExistException e){ return Response.notOk("job.not.exist"); } catch (JobServerException e){ return Response.notOk(e.getMessage()); } catch (Exception e) { Logs.info("failed to trigger job(jobId={}), cause: {}", jobId, Throwables.getStackTraceAsString(e)); return Response.notOk("job.trigger.failed"); } } @Override public Response<Boolean> notifyJob(Long jobId) { try { checkJobState(jobId, JobState.WAITING, JobState.RUNNING); return Response.ok(doOperateJob(jobId, JOB_NOTIFY + "/" + jobId)); } catch (JobFindException e){ return Response.notOk("job.find.failed"); } catch (JobStateTransferInvalidException e){ return Response.notOk("job.state.operate.invalid"); } catch (JobNotExistException e){ return Response.notOk("job.not.exist"); } catch (JobServerException e){ return Response.notOk(e.getMessage()); } catch (Exception e) { Logs.info("failed to notify job(jobId={}), cause: {}", jobId, Throwables.getStackTraceAsString(e)); return Response.notOk("job.notify.failed"); } } @Override public Response<Boolean> pauseJob(Long jobId) { try { checkJobState(jobId, null, JobState.PAUSED); return Response.ok(doOperateJob(jobId, JOB_PAUSE + "/" + jobId)); } catch (JobFindException e){ return Response.notOk("job.find.failed"); } catch (JobStateTransferInvalidException e){ return Response.notOk("job.state.operate.invalid"); } catch (JobNotExistException e){ return Response.notOk("job.not.exist"); } catch (JobServerException e){ return Response.notOk(e.getMessage()); } catch (Exception e) { Logs.info("failed to pause job(jobId={}), cause: {}", jobId, Throwables.getStackTraceAsString(e)); return Response.notOk("job.pause.failed"); } } @Override public Response<Boolean> resumeJob(Long jobId) { try { checkJobState(jobId, JobState.PAUSED, JobState.WAITING); return Response.ok(doOperateJob(jobId, JOB_RESUME + "/" + jobId)); } catch (JobServerException e){ return Response.notOk(e.getMessage()); } catch (Exception e) { Logs.info("failed to resume job(jobId={}), cause: {}", jobId, Throwables.getStackTraceAsString(e)); return Response.notOk("job.resume.failed"); } } @Override public Response<Boolean> removeJob(Long jobId) { try { checkJobState(jobId, null, JobState.STOPPED); doOperateJob(jobId, JOB_REMOVE + "/" + jobId); return Response.ok(Boolean.TRUE); } catch (JobServerException e){ return Response.ok(true); } catch (Exception e) { Logs.info("failed to remove job(jobId={}), cause: {}", jobId, Throwables.getStackTraceAsString(e)); return Response.notOk("job.remove.failed"); } } @Override public Response<Boolean> reloadJob(Long jobId) { try { return Response.ok(doOperateJob(jobId, JOB_RELOAD + "/" + jobId)); } catch (JobServerException e){ return Response.notOk(e.getMessage()); } catch (Exception e) { Logs.info("failed to reload job(jobId={}), cause: {}", jobId, Throwables.getStackTraceAsString(e)); return Response.notOk("job.reload.failed"); } } private Boolean doOperateJob(Long jobId, String uri) throws ExecutionException, RetryException { String server = getScheduleServer(jobId); if (Strings.isNullOrEmpty(server)){ Logs.warn("The job({}) isn't scheduling when call {}.", jobId, uri); return Boolean.FALSE; } return serverRetryer.call(new RetryableServerTask(server, uri)); } /** * Get the job's current schedule server * @param jobId the job id * @return the job schedule server */ private String getScheduleServer(Long jobId){ Response<String> serverResp = jobService.findServerOfJob(jobId); if (!serverResp.isSuccess()){ throw new JobServerException(serverResp.getErr().toString()); } String server = serverResp.getData(); if (Strings.isNullOrEmpty(server)){ throw new JobServerException("job.not.scheduled.by.server"); } return server; } private void checkJobState(Long jobId, JobState expectState, JobState targetState) { Response<JobDetail> jobResp = jobService.findJobDetailById(jobId); if(!jobResp.isSuccess()){ throw new JobFindException(); } JobDetail jobDetail = jobResp.getData(); if (jobDetail == null){ Logs.warn("The job(id={}) isn't exist.", jobId); throw new JobNotExistException(jobId); } String appName = jobDetail.getApp().getAppName(); String jobClass = jobDetail.getJob().getClazz(); jobSupport.checkJobStateOperate(appName, jobClass, expectState, targetState); } /** * The task for invoking the server */ private class RetryableServerTask implements Callable<Boolean> { private String server; private String uri; private Map<String, String> headers; private Map<String, Object> params; public RetryableServerTask(String server, String uri) { this.server = server; this.uri = uri; } @Override public Boolean call() throws Exception { return doRequest(server, uri, HttpMethod.POST, headers, params, 0); } } private Boolean doRequest(String server, String uri, HttpMethod method, Map<String, String> headers, Map<String, Object> params, int readTimeout){ try { String reqUri = Constants.HTTP_PREFIX + server + SERVERS + uri; Http http; if (method == HttpMethod.GET){ http = Http.get(reqUri); } else { http = Http.post(reqUri); } if (readTimeout > 0){ http.readTimeout(readTimeout); } if (headers != null){ http.headers(headers); } if (params != null){ http.params(params); } String resp = http.request(); return "true".equals(resp); } catch (HttpRequest.HttpRequestException e){ Logs.warn("The server isn't available now.", server); return Boolean.TRUE; } catch (Exception e){ Logs.error("failed to request server(server={}, uri={}, params={}), cause: {}", server, uri, params, Throwables.getStackTraceAsString(e)); return Boolean.FALSE; } } }