From f1e4d10be3b0cdcf54ec5c8959204ac88eb10684 Mon Sep 17 00:00:00 2001 From: Oliver Giles Date: Sat, 12 May 2018 17:56:56 +0300 Subject: [PATCH] resolves #36: queue/start/run --- UserManual.md | 25 ++++++++++++++++++++++--- src/client.cpp | 41 +++++++++++++++++++++++++++++++++-------- src/laminar.capnp | 11 ++++++----- src/laminar.cpp | 3 +++ src/run.h | 1 + src/server.cpp | 31 +++++++++++++++++++++++++------ test/test-server.cpp | 4 ++-- 7 files changed, 92 insertions(+), 24 deletions(-) diff --git a/UserManual.md b/UserManual.md index 4b2536a..0a796a7 100644 --- a/UserManual.md +++ b/UserManual.md @@ -104,16 +104,35 @@ chmod +x /var/lib/laminar/cfg/hello.run # Triggering a run -To trigger a run of the `hello` job, execute +When triggering a run, the job is first added to a queue of upcoming tasks. If the server is busy, the job may wait in this queue for a while. It will only be assigned a job number when it leaves this queue and starts executing. The job number may be useful to the client that triggers the run, so there are a few ways to trigger a run. + +To add the `hello` job to the queue ("fire-and-forget"), execute ```bash -laminarc trigger hello +laminarc queue hello ``` -This causes the `/var/lib/laminar/cfg/hello.run` script to be executed, with a working directory of `/var/lib/laminar/run/hello/1` +In this case, laminarc returns immediately, with its error code indicating whether adding the job to the queue was sucessful. + +To queue the job and wait until it leaves the queue and starts executing, use + +```bash +laminarc start hello +``` + +In this case, laminarc blocks until the job starts executing, or returns immediately if queueing failed. The run number will be printed to standard output. + +To launch and run the `hello` job to completion, execute + +```bash +laminarc run hello +``` + +In all cases, a started run means the `/var/lib/laminar/cfg/hello.run` script will be executed, with a working directory of `/var/lib/laminar/run/hello/1` (or current run number) The result and log output should be visible in the Web UI at http://localhost:8080/jobs/hello/1 + ## Isn't there a "Build Now" button I can click? This is against the design principles of Laminar and was deliberately excluded. Laminar's web UI is strictly read-only, making it simple to deploy in mixed-permission or public environments without an authentication layer. Furthermore, Laminar tries to encourage ideal continuous integration, where manual triggering is an anti-pattern. Want to make a release? Push a git tag and implement a post-receive hook. Want to re-run a build due to sporadic failure/flaky tests? Fix the tests locally and push a patch. Experience shows that a manual trigger such as a "Build Now" button is often used as a crutch to avoid doing the correct thing, negatively impacting traceability and quality. diff --git a/src/client.cpp b/src/client.cpp index e1835c0..abbb18e 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -84,16 +84,16 @@ int main(int argc, char** argv) { auto& waitScope = client.getWaitScope(); - if(strcmp(argv[1], "trigger") == 0) { + if(strcmp(argv[1], "queue") == 0) { if(argc < 3) { - fprintf(stderr, "Usage %s trigger \n", argv[0]); + fprintf(stderr, "Usage %s queue \n", argv[0]); return EINVAL; } - kj::Vector> promises; + kj::Vector> promises; int jobNameIndex = 2; // make a request for each job specified on the commandline do { - auto req = laminar.triggerRequest(); + auto req = laminar.queueRequest(); req.setJobName(argv[jobNameIndex]); int n = setParams(argc - jobNameIndex - 1, &argv[jobNameIndex + 1], req); promises.add(req.send()); @@ -106,14 +106,39 @@ int main(int argc, char** argv) { return ENOENT; } } - } else if(strcmp(argv[1], "run") == 0 || strcmp(argv[1], "start") == 0) { + } else if(strcmp(argv[1], "start") == 0 || strcmp(argv[1], "trigger") == 0) { + if(strcmp(argv[1], "trigger") == 0) + fprintf(stderr, "Warning: 'trigger' is deprecated, use 'queue' for the old behavior\n"); + if(argc < 3) { + fprintf(stderr, "Usage %s queue \n", argv[0]); + return EINVAL; + } + kj::Vector> promises; + struct: public kj::TaskSet::ErrorHandler { + void taskFailed(kj::Exception&&) override {} + } ignoreFailed; + kj::TaskSet ts(ignoreFailed); + int jobNameIndex = 2; + // make a request for each job specified on the commandline + do { + auto req = laminar.startRequest(); + req.setJobName(argv[jobNameIndex]); + int n = setParams(argc - jobNameIndex - 1, &argv[jobNameIndex + 1], req); + ts.add(req.send().then([&ret,argv,jobNameIndex](capnp::Response resp){ + if(resp.getResult() != LaminarCi::MethodResult::SUCCESS) { + fprintf(stderr, "Failed to start job '%s'\n", argv[2]); + ret = ENOENT; + } + printf("%s:%d\n", argv[jobNameIndex], resp.getBuildNum()); + })); + jobNameIndex += n + 1; + } while(jobNameIndex < argc); + ts.onEmpty().wait(waitScope); + } else if(strcmp(argv[1], "run") == 0) { if(argc < 3) { fprintf(stderr, "Usage %s run \n", argv[0]); return EINVAL; } - if(strcmp(argv[1], "start") == 0) { - fprintf(stderr, "Warning: \"start\" is deprecated, please use \"run\" instead\n"); - } struct: public kj::TaskSet::ErrorHandler { void taskFailed(kj::Exception&&) override {} } ignoreFailed; diff --git a/src/laminar.capnp b/src/laminar.capnp index 3b467f2..a5701f4 100644 --- a/src/laminar.capnp +++ b/src/laminar.capnp @@ -2,11 +2,12 @@ interface LaminarCi { - trigger @0 (jobName :Text, params :List(JobParam)) -> (result :MethodResult); - run @1 (jobName :Text, params :List(JobParam)) -> (result :JobResult, buildNum :UInt32); - set @2 (jobName :Text, buildNum :UInt32, param :JobParam) -> (result :MethodResult); - lock @3 (lockName :Text) -> (); - release @4 (lockName :Text) -> (); + queue @0 (jobName :Text, params :List(JobParam)) -> (result :MethodResult); + start @1 (jobName :Text, params :List(JobParam)) -> (result :MethodResult, buildNum :UInt32); + run @2 (jobName :Text, params :List(JobParam)) -> (result :JobResult, buildNum :UInt32); + set @3 (jobName :Text, buildNum :UInt32, param :JobParam) -> (result :MethodResult); + lock @4 (lockName :Text) -> (); + release @5 (lockName :Text) -> (); struct JobParam { name @0 :Text; diff --git a/src/laminar.cpp b/src/laminar.cpp index cd685be..5121d90 100644 --- a/src/laminar.cpp +++ b/src/laminar.cpp @@ -712,6 +712,9 @@ void Laminar::assignNewJobs() { c->sendMessage(msg); } + // notify the rpc client if the start command was used + run->started.fulfiller->fulfill(); + // setup run completion handler run->notifyCompletion = [this](Run* r) { runFinished(r); }; diff --git a/src/run.h b/src/run.h index 59bcc02..d8e7e3e 100644 --- a/src/run.h +++ b/src/run.h @@ -92,6 +92,7 @@ public: int fd; std::unordered_map params; kj::Promise timeout = kj::NEVER_DONE; + kj::PromiseFulfillerPair started = kj::newPromiseAndFulfiller(); time_t queuedAt; time_t startedAt; diff --git a/src/server.cpp b/src/server.cpp index 6f6848a..419418c 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -90,10 +90,10 @@ public: laminar.deregisterWaiter(this); } - // Start a job, without waiting for it to finish - kj::Promise trigger(TriggerContext context) override { + // Queue a job, without waiting for it to start + kj::Promise queue(QueueContext context) override { std::string jobName = context.getParams().getJobName(); - LLOG(INFO, "RPC trigger", jobName); + LLOG(INFO, "RPC queue", jobName); ParamMap params; for(auto p : context.getParams().getParams()) { params[p.getName().cStr()] = p.getValue().cStr(); @@ -105,6 +105,26 @@ public: return kj::READY_NOW; } + // Start a job, without waiting for it to finish + kj::Promise start(StartContext context) override { + std::string jobName = context.getParams().getJobName(); + LLOG(INFO, "RPC start", jobName); + ParamMap params; + for(auto p : context.getParams().getParams()) { + params[p.getName().cStr()] = p.getValue().cStr(); + } + std::shared_ptr run = laminar.queueJob(jobName, params); + if(Run* r = run.get()) { + return r->started.promise.then([context,r]() mutable { + context.getResults().setResult(LaminarCi::MethodResult::SUCCESS); + context.getResults().setBuildNum(r->build); + }); + } else { + context.getResults().setResult(LaminarCi::MethodResult::FAILED); + return kj::READY_NOW; + } + } + // Start a job and wait for the result kj::Promise run(RunContext context) override { std::string jobName = context.getParams().getJobName(); @@ -115,11 +135,10 @@ public: } std::shared_ptr run = laminar.queueJob(jobName, params); if(const Run* r = run.get()) { - uint num = r->build; runWaiters[r].emplace_back(kj::newPromiseAndFulfiller()); - return runWaiters[r].back().promise.then([context,num](RunState state) mutable { + return runWaiters[r].back().promise.then([context,run](RunState state) mutable { context.getResults().setResult(fromRunState(state)); - context.getResults().setBuildNum(num); + context.getResults().setBuildNum(run->build); }); } else { context.getResults().setResult(LaminarCi::JobResult::UNKNOWN); diff --git a/test/test-server.cpp b/test/test-server.cpp index 555b594..27b4187 100644 --- a/test/test-server.cpp +++ b/test/test-server.cpp @@ -98,8 +98,8 @@ protected: Server* server; }; -TEST_F(ServerTest, RpcTrigger) { - auto req = client().triggerRequest(); +TEST_F(ServerTest, RpcQueue) { + auto req = client().queueRequest(); req.setJobName("foo"); EXPECT_CALL(mockLaminar, queueJob("foo", ParamMap())).Times(testing::Exactly(1)); req.send().wait(ws());