#include "qemu/id.h"
#include "qemu/main-loop.h"
#include "trace-root.h"
+#include "qapi/qapi-events-job.h"
static QLIST_HEAD(, Job) jobs = QLIST_HEAD_INITIALIZER(jobs);
[JOB_VERB_DISMISS] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0},
};
+/* Transactional group of jobs */
+struct JobTxn {
+
+ /* Is this txn being cancelled? */
+ bool aborting;
+
+ /* List of jobs */
+ QLIST_HEAD(, Job) jobs;
+
+ /* Reference count */
+ int refcnt;
+};
+
/* Right now, this mutex is only needed to synchronize accesses to job->busy
* and job->sleep_timer, such as concurrent calls to job_do_yield and
* job_enter. */
qemu_mutex_init(&job_mutex);
}
-/* TODO Make static once the whole state machine is in job.c */
-void job_state_transition(Job *job, JobStatus s1)
+JobTxn *job_txn_new(void)
+{
+ JobTxn *txn = g_new0(JobTxn, 1);
+ QLIST_INIT(&txn->jobs);
+ txn->refcnt = 1;
+ return txn;
+}
+
+static void job_txn_ref(JobTxn *txn)
+{
+ txn->refcnt++;
+}
+
+void job_txn_unref(JobTxn *txn)
+{
+ if (txn && --txn->refcnt == 0) {
+ g_free(txn);
+ }
+}
+
+void job_txn_add_job(JobTxn *txn, Job *job)
+{
+ if (!txn) {
+ return;
+ }
+
+ assert(!job->txn);
+ job->txn = txn;
+
+ QLIST_INSERT_HEAD(&txn->jobs, job, txn_list);
+ job_txn_ref(txn);
+}
+
+static void job_txn_del_job(Job *job)
+{
+ if (job->txn) {
+ QLIST_REMOVE(job, txn_list);
+ job_txn_unref(job->txn);
+ job->txn = NULL;
+ }
+}
+
+static int job_txn_apply(JobTxn *txn, int fn(Job *), bool lock)
+{
+ AioContext *ctx;
+ Job *job, *next;
+ int rc = 0;
+
+ QLIST_FOREACH_SAFE(job, &txn->jobs, txn_list, next) {
+ if (lock) {
+ ctx = job->aio_context;
+ aio_context_acquire(ctx);
+ }
+ rc = fn(job);
+ if (lock) {
+ aio_context_release(ctx);
+ }
+ if (rc) {
+ break;
+ }
+ }
+ return rc;
+}
+
+bool job_is_internal(Job *job)
+{
+ return (job->id == NULL);
+}
+
+static void job_state_transition(Job *job, JobStatus s1)
{
JobStatus s0 = job->status;
assert(s1 >= 0 && s1 <= JOB_STATUS__MAX);
JobStatus_str(s0), JobStatus_str(s1));
assert(JobSTT[s0][s1]);
job->status = s1;
+
+ if (!job_is_internal(job) && s1 != s0) {
+ qapi_event_send_job_status_change(job->id, job->status, &error_abort);
+ }
}
int job_apply_verb(Job *job, JobVerb verb, Error **errp)
return job->cancelled;
}
+bool job_is_ready(Job *job)
+{
+ switch (job->status) {
+ case JOB_STATUS_UNDEFINED:
+ case JOB_STATUS_CREATED:
+ case JOB_STATUS_RUNNING:
+ case JOB_STATUS_PAUSED:
+ case JOB_STATUS_WAITING:
+ case JOB_STATUS_PENDING:
+ case JOB_STATUS_ABORTING:
+ case JOB_STATUS_CONCLUDED:
+ case JOB_STATUS_NULL:
+ return false;
+ case JOB_STATUS_READY:
+ case JOB_STATUS_STANDBY:
+ return true;
+ default:
+ g_assert_not_reached();
+ }
+ return false;
+}
+
bool job_is_completed(Job *job)
{
switch (job->status) {
return false;
}
-bool job_started(Job *job)
+static bool job_started(Job *job)
{
return job->co;
}
-bool job_should_pause(Job *job)
+static bool job_should_pause(Job *job)
{
return job->pause_count > 0;
}
job_enter(job);
}
-void *job_create(const char *job_id, const JobDriver *driver, AioContext *ctx,
- int flags, BlockCompletionFunc *cb, void *opaque, Error **errp)
+void *job_create(const char *job_id, const JobDriver *driver, JobTxn *txn,
+ AioContext *ctx, int flags, BlockCompletionFunc *cb,
+ void *opaque, Error **errp)
{
Job *job;
notifier_list_init(&job->on_finalize_cancelled);
notifier_list_init(&job->on_finalize_completed);
notifier_list_init(&job->on_pending);
+ notifier_list_init(&job->on_ready);
job_state_transition(job, JOB_STATUS_CREATED);
aio_timer_init(qemu_get_aio_context(), &job->sleep_timer,
QLIST_INSERT_HEAD(&jobs, job, job_list);
+ /* Single jobs are modeled as single-job transactions for sake of
+ * consolidating the job management logic */
+ if (!txn) {
+ txn = job_txn_new();
+ job_txn_add_job(txn, job);
+ job_txn_unref(txn);
+ } else {
+ job_txn_add_job(txn, job);
+ }
+
return job;
}
if (--job->refcnt == 0) {
assert(job->status == JOB_STATUS_NULL);
assert(!timer_pending(&job->sleep_timer));
+ assert(!job->txn);
if (job->driver->free) {
job->driver->free(job);
QLIST_REMOVE(job, job_list);
+ g_free(job->error);
g_free(job->id);
g_free(job);
}
}
+void job_progress_update(Job *job, uint64_t done)
+{
+ job->progress_current += done;
+}
+
+void job_progress_set_remaining(Job *job, uint64_t remaining)
+{
+ job->progress_total = job->progress_current + remaining;
+}
+
+void job_progress_increase_remaining(Job *job, uint64_t delta)
+{
+ job->progress_total += delta;
+}
+
void job_event_cancelled(Job *job)
{
notifier_list_notify(&job->on_finalize_cancelled, job);
notifier_list_notify(&job->on_finalize_completed, job);
}
-void job_event_pending(Job *job)
+static void job_event_pending(Job *job)
{
notifier_list_notify(&job->on_pending, job);
}
+static void job_event_ready(Job *job)
+{
+ notifier_list_notify(&job->on_ready, job);
+}
+
void job_enter_cond(Job *job, bool(*fn)(Job *job))
{
if (!job_started(job)) {
}
/* Yield, and schedule a timer to reenter the coroutine after @ns nanoseconds.
- * Reentering the job coroutine with block_job_enter() before the timer has
- * expired is allowed and cancels the timer.
+ * Reentering the job coroutine with job_enter() before the timer has expired
+ * is allowed and cancels the timer.
*
- * If @ns is (uint64_t) -1, no timer is scheduled and block_job_enter() must be
+ * If @ns is (uint64_t) -1, no timer is scheduled and job_enter() must be
* called explicitly. */
-void coroutine_fn job_do_yield(Job *job, uint64_t ns)
+static void coroutine_fn job_do_yield(Job *job, uint64_t ns)
{
job_lock();
if (ns != -1) {
}
}
+void job_yield(Job *job)
+{
+ assert(job->busy);
+
+ /* Check cancellation *before* setting busy = false, too! */
+ if (job_is_cancelled(job)) {
+ return;
+ }
+
+ if (!job_should_pause(job)) {
+ job_do_yield(job, -1);
+ }
+
+ job_pause_point(job);
+}
+
void coroutine_fn job_sleep_ns(Job *job, int64_t ns)
{
assert(job->busy);
job_resume(job);
}
-void job_do_dismiss(Job *job)
+static void job_do_dismiss(Job *job)
{
assert(job);
job->busy = false;
job->paused = false;
job->deferred_to_main_loop = true;
- /* TODO Don't assume it's a BlockJob */
- block_job_txn_del_job((BlockJob*) job);
+ job_txn_del_job(job);
job_state_transition(job, JOB_STATUS_NULL);
job_unref(job);
}
+void job_dismiss(Job **jobptr, Error **errp)
+{
+ Job *job = *jobptr;
+ /* similarly to _complete, this is QMP-interface only. */
+ assert(job->id);
+ if (job_apply_verb(job, JOB_VERB_DISMISS, errp)) {
+ return;
+ }
+
+ job_do_dismiss(job);
+ *jobptr = NULL;
+}
+
void job_early_fail(Job *job)
{
assert(job->status == JOB_STATUS_CREATED);
}
}
-void job_update_rc(Job *job)
+static void job_update_rc(Job *job)
{
if (!job->ret && job_is_cancelled(job)) {
job->ret = -ECANCELED;
}
if (job->ret) {
+ if (!job->error) {
+ job->error = g_strdup(strerror(-job->ret));
+ }
job_state_transition(job, JOB_STATUS_ABORTING);
}
}
}
}
-int job_finalize_single(Job *job)
+static int job_finalize_single(Job *job)
{
assert(job_is_completed(job));
}
}
- /* TODO Don't assume it's a BlockJob */
- block_job_txn_del_job((BlockJob*) job);
+ job_txn_del_job(job);
job_conclude(job);
return 0;
}
+static void job_cancel_async(Job *job, bool force)
+{
+ if (job->user_paused) {
+ /* Do not call job_enter here, the caller will handle it. */
+ job->user_paused = false;
+ if (job->driver->user_resume) {
+ job->driver->user_resume(job);
+ }
+ assert(job->pause_count > 0);
+ job->pause_count--;
+ }
+ job->cancelled = true;
+ /* To prevent 'force == false' overriding a previous 'force == true' */
+ job->force_cancel |= force;
+}
+
+static void job_completed_txn_abort(Job *job)
+{
+ AioContext *ctx;
+ JobTxn *txn = job->txn;
+ Job *other_job;
+
+ if (txn->aborting) {
+ /*
+ * We are cancelled by another job, which will handle everything.
+ */
+ return;
+ }
+ txn->aborting = true;
+ job_txn_ref(txn);
+
+ /* We are the first failed job. Cancel other jobs. */
+ QLIST_FOREACH(other_job, &txn->jobs, txn_list) {
+ ctx = other_job->aio_context;
+ aio_context_acquire(ctx);
+ }
+
+ /* Other jobs are effectively cancelled by us, set the status for
+ * them; this job, however, may or may not be cancelled, depending
+ * on the caller, so leave it. */
+ QLIST_FOREACH(other_job, &txn->jobs, txn_list) {
+ if (other_job != job) {
+ job_cancel_async(other_job, false);
+ }
+ }
+ while (!QLIST_EMPTY(&txn->jobs)) {
+ other_job = QLIST_FIRST(&txn->jobs);
+ ctx = other_job->aio_context;
+ if (!job_is_completed(other_job)) {
+ assert(job_is_cancelled(other_job));
+ job_finish_sync(other_job, NULL, NULL);
+ }
+ job_finalize_single(other_job);
+ aio_context_release(ctx);
+ }
+
+ job_txn_unref(txn);
+}
+
+static int job_prepare(Job *job)
+{
+ if (job->ret == 0 && job->driver->prepare) {
+ job->ret = job->driver->prepare(job);
+ job_update_rc(job);
+ }
+ return job->ret;
+}
+
+static int job_needs_finalize(Job *job)
+{
+ return !job->auto_finalize;
+}
+
+static void job_do_finalize(Job *job)
+{
+ int rc;
+ assert(job && job->txn);
+
+ /* prepare the transaction to complete */
+ rc = job_txn_apply(job->txn, job_prepare, true);
+ if (rc) {
+ job_completed_txn_abort(job);
+ } else {
+ job_txn_apply(job->txn, job_finalize_single, true);
+ }
+}
+
+void job_finalize(Job *job, Error **errp)
+{
+ assert(job && job->id);
+ if (job_apply_verb(job, JOB_VERB_FINALIZE, errp)) {
+ return;
+ }
+ job_do_finalize(job);
+}
+
+static int job_transition_to_pending(Job *job)
+{
+ job_state_transition(job, JOB_STATUS_PENDING);
+ if (!job->auto_finalize) {
+ job_event_pending(job);
+ }
+ return 0;
+}
+
+void job_transition_to_ready(Job *job)
+{
+ job_state_transition(job, JOB_STATUS_READY);
+ job_event_ready(job);
+}
+
+static void job_completed_txn_success(Job *job)
+{
+ JobTxn *txn = job->txn;
+ Job *other_job;
+
+ job_state_transition(job, JOB_STATUS_WAITING);
+
+ /*
+ * Successful completion, see if there are other running jobs in this
+ * txn.
+ */
+ QLIST_FOREACH(other_job, &txn->jobs, txn_list) {
+ if (!job_is_completed(other_job)) {
+ return;
+ }
+ assert(other_job->ret == 0);
+ }
+
+ job_txn_apply(txn, job_transition_to_pending, false);
+
+ /* If no jobs need manual finalization, automatically do so */
+ if (job_txn_apply(txn, job_needs_finalize, false) == 0) {
+ job_do_finalize(job);
+ }
+}
+
+void job_completed(Job *job, int ret, Error *error)
+{
+ assert(job && job->txn && !job_is_completed(job));
+
+ job->ret = ret;
+ if (error) {
+ assert(job->ret < 0);
+ job->error = g_strdup(error_get_pretty(error));
+ error_free(error);
+ }
+
+ job_update_rc(job);
+ trace_job_completed(job, ret, job->ret);
+ if (job->ret) {
+ job_completed_txn_abort(job);
+ } else {
+ job_completed_txn_success(job);
+ }
+}
+
+void job_cancel(Job *job, bool force)
+{
+ if (job->status == JOB_STATUS_CONCLUDED) {
+ job_do_dismiss(job);
+ return;
+ }
+ job_cancel_async(job, force);
+ if (!job_started(job)) {
+ job_completed(job, -ECANCELED, NULL);
+ } else if (job->deferred_to_main_loop) {
+ job_completed_txn_abort(job);
+ } else {
+ job_enter(job);
+ }
+}
+
+void job_user_cancel(Job *job, bool force, Error **errp)
+{
+ if (job_apply_verb(job, JOB_VERB_CANCEL, errp)) {
+ return;
+ }
+ job_cancel(job, force);
+}
+
+/* A wrapper around job_cancel() taking an Error ** parameter so it may be
+ * used with job_finish_sync() without the need for (rather nasty) function
+ * pointer casts there. */
+static void job_cancel_err(Job *job, Error **errp)
+{
+ job_cancel(job, false);
+}
+
+int job_cancel_sync(Job *job)
+{
+ return job_finish_sync(job, &job_cancel_err, NULL);
+}
+
+void job_cancel_sync_all(void)
+{
+ Job *job;
+ AioContext *aio_context;
+
+ while ((job = job_next(NULL))) {
+ aio_context = job->aio_context;
+ aio_context_acquire(aio_context);
+ job_cancel_sync(job);
+ aio_context_release(aio_context);
+ }
+}
+
+int job_complete_sync(Job *job, Error **errp)
+{
+ return job_finish_sync(job, job_complete, errp);
+}
+
void job_complete(Job *job, Error **errp)
{
/* Should not be reachable via external interface for internal jobs */