/* ************************************************************************ # # DivConq # # http://divconq.com/ # # Copyright: # Copyright 2014 eTimeline, LLC. All rights reserved. # # License: # See the license.txt file in the project's top-level directory for details. # # Authors: # * Andy White # ************************************************************************ */ /** * All database access required to support DivConq Work Queues goes into * this class. Data access is abstracted out in DC so that we can later * use other storage models. */ package divconq.work.sql; import divconq.hub.Hub; import divconq.lang.op.FuncResult; import divconq.lang.op.OperationContext; import divconq.lang.op.OperationResult; import divconq.sql.SqlEngine; import divconq.sql.SqlManager.SqlDatabase; import divconq.sql.SqlNull; import divconq.sql.SqlSelect; import divconq.sql.SqlSelectInteger; import divconq.sql.SqlSelectSqlDateTime; import divconq.sql.SqlSelectString; import divconq.struct.ListStruct; import divconq.struct.RecordStruct; import divconq.struct.Struct; import divconq.util.StringUtil; import divconq.work.IQueueDriver; import divconq.work.TaskRun; import divconq.work.Task; import divconq.xml.XElement; public class QueueDriver implements IQueueDriver { @Override public void init(OperationResult or, XElement config) { } @Override public void start(OperationResult or) { or.infoTr(160); } @Override public void stop(OperationResult or) { or.infoTr(161); } // return candidate list from [ TaskIdentity: "vvv", WorkId: nnn, ClaimedStamp: "vvv" ] // where stamp is a string of yyyy-mm-dd hh:mm:ss.mmm @Override public FuncResult<ListStruct> findPotentialClaims(String pool, int howmanymax) { FuncResult<ListStruct> res = new FuncResult<ListStruct>(); SqlDatabase db = Hub.instance.getSQLDatabase(); if (db == null) { res.errorTr(156); return res; } // don't use local time for timeout stuff - use database time? // h2 syntax String where = "(dcClaimedStamp < DATEADD('MINUTE', -dcClaimTimeout, " + db.nowFunc() + ")) "; if (db.getEngine() == SqlEngine.SqlServer) where = "(dcClaimedStamp < DATEADD(MINUTE, -dcClaimTimeout, " + db.nowFunc() + ")) "; if (db.getEngine() == SqlEngine.MariaDb || db.getEngine() == SqlEngine.MySQL) where = "(dcClaimedStamp < DATE_SUB(" + db.nowFunc() + ", INTERVAL dcClaimTimeout minute)) "; where += "AND (dcDestBucket = ?) " + "AND ((dcDestSquad = ?) OR (dcDestSquad IS NULL)) AND ((dcDestHubId = ?) OR (dcDestHubId IS NULL))"; FuncResult<ListStruct> rsres = db.executeQueryLimit( new SqlSelect[] { new SqlSelectString("dcTaskIdentity", "TaskIdentity", null), new SqlSelectSqlDateTime("dcClaimedStamp", "ClaimedStamp", null, true), new SqlSelectInteger("dcWorkId", "WorkId", 0), new SqlSelectInteger("dcClaimTimeout", "ClaimTimeout", 0), new SqlSelectSqlDateTime("dcAddedStamp", "AddedStamp", null) }, "dcWorkQueue", // from where, // where null, // group by "AddedStamp", // order by howmanymax, // limit false, pool, // param 1 - pool name Hub.instance.getResources().getSquadId(), // param 2 - squad name Hub.instance.getResources().getHubId() // param 3 - hub id ); ListStruct rs = rsres.getResult(); res.setResult(rs); return res; } // confirm a claim on a work queue entry, error if not able to claim // return the stamp used // where stamp is a string of yyyy-mm-dd hh:mm:ss.mmm @Override public FuncResult<RecordStruct> makeClaim(RecordStruct info) { FuncResult<RecordStruct> res = new FuncResult<RecordStruct>(); Hub.instance.getCountManager().countObjects("dcWorkQueueClaims", info); String taskidentity = info.getFieldAsString("TaskIdentity"); String oldstamp = info.getFieldAsString("ClaimedStamp"); SqlDatabase db = Hub.instance.getSQLDatabase(); if (db == null) { res.errorTr(156); Hub.instance.getCountManager().countObjects("dcWorkQueueClaimFails", info); return res; } // MAKE OUR CLAIM String sql = "UPDATE dcWorkQueue SET dcClaimedStamp = " + db.nowFunc() + ", dcClaimedHub = ? WHERE dcTaskIdentity = ? AND dcClaimedStamp = ?"; FuncResult<Integer> ures = db.executeUpdate(sql, Hub.instance.getResources().getHubId(), // param 1 - hub id making the claim taskidentity, // param 2 - task to claim oldstamp // param 3 - time stamp we had when last we heard about this task - if we are out of date then we don't earn the task ); if (ures.getResult() != 1) { res.errorTr(162, taskidentity); Hub.instance.getCountManager().countObjects("dcWorkQueueClaimFails", info); return res; } // UPDATE OUR KNOWN STAMP - without this we cannot continue to talk to this task record // this must execute within 1 minute of code above - which shouldn't be a problem, but just FYI // if it does not it could get treated as timeout and someone else make a claim FuncResult<Struct> rsres = db.executeQueryScalar( new SqlSelectSqlDateTime("dcClaimedStamp", null, true), "dcWorkQueue", // from "dcClaimedHub = ? AND dcTaskIdentity = ?", // where null, Hub.instance.getResources().getHubId(), // param 1 - hub id taskidentity // param 2 - task to claim ); info.setField("ClaimedStamp", Struct.objectToString(rsres.getResult())); res.setResult(info); return res; } // update claim stamp on a work queue entry, error if not able to claim // return the stamp used // where stamp is a string of yyyy-mm-dd hh:mm:ss.mmm @Override public OperationResult updateClaim(Task info) { OperationResult res = new OperationResult(); Hub.instance.getCountManager().countObjects("dcWorkQueueClaimUpdates", info); String taskidentity = info.getId(); String oldstamp = info.getClaimedStamp(); SqlDatabase db = Hub.instance.getSQLDatabase(); if (db == null) { res.errorTr(156); Hub.instance.getCountManager().countObjects("dcWorkQueueClaimUpdateFails", info); return res; } String sql = "UPDATE dcWorkQueue SET dcClaimedStamp = " + db.nowFunc() + " WHERE dcTaskIdentity = ? AND dcClaimedStamp = ?"; FuncResult<Integer> ures = db.executeUpdate(sql, taskidentity, // param 1 - task we claimed oldstamp // param 2 - time stamp we had when last we heard about this task - if we are out of date then we don't keep the task ); if (ures.getResult() != 1) { res.errorTr(188, taskidentity); Hub.instance.getCountManager().countObjects("dcWorkQueueClaimUpdateFails", info); return res; } //System.out.println(">>>> Claim updated <<<< " + taskidentity); // UPDATE OUR KNOWN STAMP - without this we cannot continue to talk to this task record FuncResult<Struct> rsres = db.executeQueryScalar( new SqlSelectSqlDateTime("dcClaimedStamp", null, true), "dcWorkQueue", // from "dcClaimedHub = ? AND dcTaskIdentity = ?", // where null, Hub.instance.getResources().getHubId(), // param 1 - hub id taskidentity // param 2 - task to claim ); Struct rs = rsres.getResult(); info.withClaimedStamp(Struct.objectToString(rs)); return res; } // return claim stamp @Override public FuncResult<String> reserveUniqueWork(String taskidentity) { FuncResult<String> res = new FuncResult<String>(); Hub.instance.getCountManager().countObjects("dcWorkQueueReserves", taskidentity); SqlDatabase db = Hub.instance.getSQLDatabase(); // put in distant future so no one else can claim/reserve it String claimstamp = "9999-01-01 00:00:00.000"; String sql = "INSERT INTO dcWorkQueue(dcTaskIdentity, dcClaimedStamp, dcAddedStamp) " + " VALUES (?,?," + db.nowFunc() + ")"; FuncResult<Integer> ires = db.executeUpdate(sql, taskidentity, // param 1 - task we are reserving claimstamp // param 2 - lock it with a future stamp ); if (ires.getCode() == 194) { res.infoTr(162, taskidentity); return res; } if (ires.getResult() != 1) { res.errorTr(183, taskidentity); Hub.instance.getCountManager().countObjects("dcWorkQueueReserveFails", taskidentity); return res; } sql = "SELECT Id FROM dcWork WHERE dcTaskIdentity = ?"; FuncResult<Struct> rsres = db.executeQueryScalar( new SqlSelectString("Id"), "dcWork", // from "dcTaskIdentity = ?", // where null, taskidentity // param 1 - task to claim ); if (rsres.isEmptyResult()) { res.setResult(claimstamp); return res; } // if there is a record here it means the work was already created and completed before we got a chance // to even add - remove our attempt at adding res.errorTr(183, taskidentity); // therefore we remove any claim we might have sql = "DELETE FROM dcWorkQueue WHERE dcTaskIdentity = ?"; db.executeUpdate(sql, taskidentity); return res; } // return claim stamp @Override public FuncResult<String> reserveCurrentWork(String taskidentity) { FuncResult<String> res = new FuncResult<String>(); Hub.instance.getCountManager().countObjects("dcWorkQueueReserves", taskidentity); SqlDatabase db = Hub.instance.getSQLDatabase(); // put in distant future so no one else can claim/reserve it String claimstamp = "9999-01-01 00:00:00.000"; String sql = "INSERT INTO dcWorkQueue(dcTaskIdentity, dcClaimedStamp, dcAddedStamp) " + " VALUES (?,?," + db.nowFunc() + ")"; FuncResult<Integer> ires = db.executeUpdate(sql, taskidentity, // param 1 - task we are reserving claimstamp // param 2 - lock it with a future stamp ); if (ires.getCode() == 194) { res.infoTr(162, taskidentity); return res; } if (ires.getResult() != 1) { res.errorTr(183, taskidentity); Hub.instance.getCountManager().countObjects("dcWorkQueueReserveFails", taskidentity); return res; } res.setResult(claimstamp); return res; } @Override public FuncResult<String> submit(Task info) { FuncResult<String> res = new FuncResult<>(); String forbucket = info.getBucket(); String taskidentity = info.getId(); ListStruct tags = info.getTags(); String tag1 = ((tags != null) && (tags.getSize() > 0)) ? tags.getItemAsString(0) : null; String tag2 = ((tags != null) && (tags.getSize() > 1)) ? tags.getItemAsString(1) : null; String tag3 = ((tags != null) && (tags.getSize() > 2)) ? tags.getItemAsString(2) : null; String forsquad = info.getSquad(); String forhub = info.getHub(); String reservedclaim = info.getClaimedStamp(); SqlDatabase db = Hub.instance.getSQLDatabase(); String sql = "INSERT INTO dcWork (dcTaskIdentity, dcAddStamp, dcTitle, dcTask, dcDestBucket, dcDestSquad, dcDestHubId, dcTag1, dcTag2, dcTag3, dcMaxTries) " + " VALUES (?," + db.nowFunc() + ",?,?,?,?,?,?,?,?,?)"; FuncResult<Long> ires = db.executeInsertReturnId(sql, taskidentity, info.getTitle(), info.freezeToRecord().toString(), forbucket, StringUtil.isNotEmpty(forsquad) ? forsquad : SqlNull.VarChar, StringUtil.isNotEmpty(forhub) ? forhub : SqlNull.VarChar, StringUtil.isNotEmpty(tag1) ? tag1 : SqlNull.VarChar, StringUtil.isNotEmpty(tag2) ? tag2 : SqlNull.VarChar, StringUtil.isNotEmpty(tag3) ? tag3 : SqlNull.VarChar, info.getMaxTries() ); long workid = ires.getResult(); if (workid == 0) { res.errorTr(158, taskidentity); return res; } int cnt = 0; if (reservedclaim == null) { sql = "INSERT INTO dcWorkQueue(dcTaskIdentity, dcWorkId, dcDestBucket, dcDestSquad, dcDestHubId, dcClaimTimeout, dcAddedStamp) " + " VALUES (?,?,?,?,?,?," + db.nowFunc() + ")"; FuncResult<Integer> ires2 = db.executeUpdate(sql, taskidentity, workid, forbucket, StringUtil.isNotEmpty(forsquad) ? forsquad : SqlNull.VarChar, StringUtil.isNotEmpty(forhub) ? forhub : SqlNull.VarChar, info.getTimeout() ); cnt = ires2.getResult(); } else { // if already reserved, then just make it available to the queue by switching back to 1970 sql = "UPDATE dcWorkQueue SET dcClaimedStamp = ?, dcWorkId = ?, dcDestBucket = ?, dcDestSquad = ?, dcDestHubId = ?, dcClaimTimeout = ? WHERE dcTaskIdentity = ? AND dcClaimedStamp = ?"; FuncResult<Integer> ires2 = db.executeUpdate(sql, "1970-01-01 00:00:00.000", // set claim stamp back to 1970 - means we are open to be claimed workid, forbucket, StringUtil.isNotEmpty(forsquad) ? forsquad : SqlNull.VarChar, StringUtil.isNotEmpty(forhub) ? forhub : SqlNull.VarChar, info.getTimeout(), taskidentity, reservedclaim ); cnt = ires2.getResult(); } if (cnt != 1) { res.errorTr(159, taskidentity); sql = "DELETE FROM dcWork WHERE Id = ?"; db.executeUpdate(sql, workid); } else res.setResult(workid + ""); Hub.instance.getCountManager().countObjects("dcWorkQueueAddWorkTotal", taskidentity); return res; } // see dcTask in schema // must pass in WorkId and ClaimedStamp to function correctly with startWork @Override public FuncResult<Task> loadWork(RecordStruct info) { long workid = info.getFieldAsInteger("WorkId"); SqlDatabase db = Hub.instance.getSQLDatabase(); FuncResult<Task> res = new FuncResult<>(); FuncResult<ListStruct> rsres = db.executeQuery( new SqlSelect[] { new SqlSelectSqlDateTime("dcAddStamp", "AddStamp", null), new SqlSelectString("dcStatus", "Status", null), new SqlSelectString("dcTask", "Task", null), new SqlSelectInteger("dcCurrentTry", "CurrentTry", 0) }, "dcWork", // from "Id = ? AND Active = 1", // where null, // group by null, // order by workid // param 1 - dcWork record id ); ListStruct rs = rsres.getResult(); if (rs.isEmpty()) { res.errorTr(166, workid); return res; } RecordStruct rec = rs.getItemAsRecord(0); Task tb = new Task(rec.getFieldAsRecord("Task")) .withWorkId(workid + "") .withClaimedStamp(info.getFieldAsString("ClaimedStamp")) .withStatus(rec.getFieldAsString("Status")) .withAddStamp(rec.getFieldAsDateTime("AddStamp")) .withCurrentTry((int)rec.getFieldAsInteger("CurrentTry", 0)); res.setResult(tb); return res; } // return audit id @Override public FuncResult<String> startWork(String workid) { FuncResult<String> res = new FuncResult<>(); SqlDatabase db = Hub.instance.getSQLDatabase(); String sql = null; FuncResult<ListStruct> rsres = db.executeQuery( new SqlSelect[] { new SqlSelectString("dcTaskIdentity", "TaskIdentity", null), new SqlSelectString("dcStatus", "Status", null), new SqlSelectInteger("dcCurrentTry", "LastTry", 0), new SqlSelectInteger("dcMaxTries", "MaxTry", 0) }, "dcWork", // from "Id = ?", // where null, // group by null, // order by StringUtil.parseInt(workid) // param 1 ); ListStruct rs = rsres.getResult(); if (rs.isEmpty()) { res.errorTr(166, workid); return res; } RecordStruct rec = rs.getItemAsRecord(0); String taskidentity = rec.getFieldAsString("TaskIdentity"); String status = rec.getFieldAsString("Status"); int trynum = (int)rec.getFieldAsInteger("LastTry", 0); int maxtries = (int)rec.getFieldAsInteger("MaxTry", 0); if ("Completed".equals(status)) { res.errorTr(167, taskidentity); return res; } trynum++; if (trynum > maxtries) { sql = "UPDATE dcWork SET dcStatus = ? WHERE Id = ?"; FuncResult<Integer> ires2 = db.executeUpdate(sql, "Failed", workid); if (ires2.getResult() != 1) res.errorTr(164, taskidentity); // delete even if error above, we should not be on queue now sql = "DELETE FROM dcWorkQueue WHERE dcTaskIdentity = ?"; ires2 = db.executeUpdate(sql, taskidentity); if (ires2.getResult() != 1) res.warnTr(165, taskidentity); // warning only because at least we know there is no entry in the queue res.errorTr(168, taskidentity); return res; } // create a new audit entry sql = "INSERT INTO dcWorkAudit (dcWorkId, dcTryNum, dcHub, dcStartStamp) " + " VALUES (?,?,?," + db.nowFunc() + ")"; int ftrynum = trynum; FuncResult<Long> ires2 = db.executeInsertReturnId(sql, workid, ftrynum, OperationContext.getHubId()); if (ires2.getResult() == 0) { res.errorTr(170, taskidentity); return res; } sql = "UPDATE dcWork SET dcCurrentTry = ?, dcLastAudit = ?, dcStatus = ? WHERE Id = ?"; FuncResult<Integer> ires3 = db.executeUpdate(sql, ftrynum, ires2.getResult(), "Running", workid); if (ires3.getResult() != 1) { res.errorTr(164, taskidentity); return res; } res.setResult(ires2.getResult() + ""); Hub.instance.getCountManager().countObjects("dcWorkQueueStarts", taskidentity); return res; } @Override public OperationResult endWork(TaskRun task) { OperationResult res = new OperationResult(); Task info = task.getTask(); boolean finaltry = task.getTask().getFinalTry(); Long auditid = StringUtil.parseInt(info.getAuditId()); Long workid = StringUtil.parseInt(info.getWorkId()); String msg = task.getMessage(); String log = task.getContext().getLog(); SqlDatabase db = Hub.instance.getSQLDatabase(); String sql = "UPDATE dcWorkAudit SET dcEndStamp = " + db.nowFunc() + ", dcCode = ?, dcMessage = ?, dcLogFile = ? WHERE Id = ?"; FuncResult<Integer> ires2 = db.executeUpdate(sql, task.getCode(), StringUtil.isNotEmpty(msg) ? msg : SqlNull.Text, StringUtil.isNotEmpty(log) ? log : SqlNull.Text, auditid ); if (ires2.getResult() != 1) res.errorTr(163, task.getTask().getId()); // continue even if error, other tables should be updated sql = "UPDATE dcWork SET dcStatus = ? WHERE Id = ?"; ires2 = db.executeUpdate(sql, info.getStatus(), workid); if (ires2.getResult() != 1) res.errorTr(164, task.getTask().getId()); if (finaltry) { sql = "DELETE FROM dcWorkQueue WHERE dcTaskIdentity = ?"; ires2 = db.executeUpdate(sql, task.getTask().getId()); if (ires2.getResult() != 1) res.warnTr(165, task.getTask().getId()); // warning only because at least we know there is no entry in the queue } Hub.instance.getCountManager().countObjects("dcWorkQueueEnds", task.getTask().getId()); return res; } @Override public OperationResult trackWork(TaskRun task, boolean ended) { OperationResult res = new OperationResult(); Task info = task.getTask(); Long auditid = StringUtil.parseInt(info.getAuditId()); String msg = task.getMessage(); String log = task.getContext().getLog(); String progress = task.getContext().getProgressMessage(); long completed = task.getContext().getAmountCompleted(); long steps = task.getContext().getSteps(); long step = task.getContext().getCurrentStep(); String sname = task.getContext().getCurrentStepName(); SqlDatabase db = Hub.instance.getSQLDatabase(); String sql = "UPDATE dcWorkAudit SET dcCode = ?, dcMessage = ?, dcLogFile = ?, dcProgress = ?, dcCompleted = ?, dcSteps = ?, dcStep = ?, dcStepName = ? WHERE Id = ?"; FuncResult<Integer> ires2 = db.executeUpdate(sql, task.getCode(), StringUtil.isNotEmpty(msg) ? msg : SqlNull.Text, StringUtil.isNotEmpty(log) ? log : SqlNull.Text, StringUtil.isNotEmpty(progress) ? progress : SqlNull.Text, completed, steps, step, StringUtil.isNotEmpty(sname) ? sname : SqlNull.Text, auditid ); if (ires2.getResult() != 1) res.errorTr(163, task.getTask().getId()); // don't overwrite endstamp, unless ended is set if (ended) { sql = "UPDATE dcWorkAudit SET dcEndStamp = " + db.nowFunc() + " WHERE Id = ?"; db.executeUpdate(sql, auditid); } return res; } @Override public ListStruct list() { ListStruct res = new ListStruct(); SqlDatabase db = Hub.instance.getSQLDatabase(); if (db == null) return res; FuncResult<ListStruct> rsres = db.executeQuery( new SqlSelect[] { new SqlSelectInteger("w.Id", "Id", 0), new SqlSelectString("w.dcTaskIdentity", "TaskIdentity", null), new SqlSelectString("w.dcTitle", "Title", null), new SqlSelectString("w.dcStatus", "Status", null), new SqlSelectSqlDateTime("q.dcClaimedStamp", "ClaimedAt", null), new SqlSelectInteger("q.dcClaimTimeout", "ClaimTimeout", 0), new SqlSelectString("q.dcClaimedHub", "ClaimedBy", null), new SqlSelectInteger("w.dcCurrentTry", "LastTry", 0), new SqlSelectInteger("w.dcMaxTries", "MaxTry", 0), new SqlSelectSqlDateTime("w.dcAddStamp", "Added", null) }, "dcWorkQueue AS q INNER JOIN dcWork AS w ON (q.dcWorkId = w.Id)", // from null, // where null, // group by "w.dcAddStamp" // order by ); if (rsres.hasErrors()) return res; for (Struct rss : rsres.getResult().getItems()) { RecordStruct rec = (RecordStruct)rss; FuncResult<ListStruct> rsres2 = db.executeQuery( new SqlSelect[] { new SqlSelectInteger("dcTryNum", "Try", 0), new SqlSelectString("dcHub", "Hub", null), new SqlSelectString("dcMessage", "Message", null), new SqlSelectInteger("dcCode", "Code", 0) }, "dcWorkAudit", // from "dcWorkId = ?", // where null, // group by "dcTryNum", // order by rec.getFieldAsInteger("Id", 0) // param 1 - work id ); rec.setField("Audit", rsres2.getResult()); res.addItem(rec); } return res; } @Override public RecordStruct status(String taskid, String workid) { SqlDatabase db = Hub.instance.getSQLDatabase(); if (db == null) return null; FuncResult<RecordStruct> rsres = db.executeQueryRecord( new SqlSelect[] { new SqlSelectString("w.Id", "WorkId", null), new SqlSelectString("w.dcTaskIdentity", "TaskId", null), new SqlSelectString("w.dcStatus", "Status", null), new SqlSelectString("w.dcTitle", "Title", null), new SqlSelectInteger("w.dcMaxTries", "MaxTry", 0), new SqlSelectSqlDateTime("w.dcAddStamp", "Added", null), new SqlSelectSqlDateTime("a.dcStartStamp", "Start", null), new SqlSelectSqlDateTime("a.dcEndStamp", "End", null), new SqlSelectString("dcHub", "Hub", null), new SqlSelectInteger("a.dcTryNum", "Try", 0), new SqlSelectInteger("a.dcCode", "Code", 0), new SqlSelectString("a.dcMessage", "Message", null), new SqlSelectString("a.dcLogFile", "Log", null), new SqlSelectString("a.dcProgress", "Progress", null), new SqlSelectString("a.dcStepName", "StepName", null), new SqlSelectInteger("a.dcCompleted", "Completed", 0), new SqlSelectInteger("a.dcStep", "Step", 0), new SqlSelectInteger("a.dcSteps", "Steps", 0) }, "dcWork AS w INNER JOIN dcWorkAudit AS a ON (a.dcWorkId = w.Id)", // from "w.Active = 1 AND w.dcTaskIdentity = ? AND w.Id = ?", // where taskid, StringUtil.parseInt(workid) ); if (rsres.hasErrors() || rsres.isEmptyResult()) return null; return rsres.getResult(); } }