{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "pycharm": {}
      },
      "source": [
        "# Advanced Job Submission and Management\n",
        "\n",
        "Launch this tutorial in a Jupyter Notebook on Binder: \n",
        "[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/htcondor/htcondor-python-bindings-tutorials/master?urlpath=lab/tree/Advanced-Job-Submission-And-Management.ipynb)\n",
        "\n",
        "The two most common HTCondor command line tools are `condor_q` and `condor_submit`.\n",
        "In the previous module, we learned about the `query()` method that corresponds to `condor_q`. Here, we will learn the Python binding equivalent of `condor_submit` in greater detail.\n",
        "\n",
        "We start by importing the relevant modules:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "metadata": {
        "pycharm": {}
      },
      "outputs": [],
      "source": [
        "import htcondor"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "pycharm": {}
      },
      "source": [
        "Submitting Jobs\n",
        "---------------\n",
        "\n",
        "We will submit jobs utilizing the dedicated `Submit` object.\n",
        "\n",
        "`Submit` objects consist of key-value pairs. Unlike ClassAds, the values do not have an inherent type (such as strings, integers, or booleans); they are evaluated with macro expansion at submit time. Where reasonable, they behave like Python dictionaries:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "metadata": {
        "pycharm": {}
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "foo = 1\n",
            "bar = 2\n",
            "baz = $(foo)\n",
            "\n"
          ]
        }
      ],
      "source": [
        "sub = htcondor.Submit({\"foo\": \"1\", \"bar\": \"2\", \"baz\": \"$(foo)\"})\n",
        "print(sub)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "metadata": {
        "pycharm": {}
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "foo = 1\n",
            "bar = 2\n",
            "baz = $(foo)\n",
            "qux = 3\n",
            "\n"
          ]
        }
      ],
      "source": [
        "sub[\"qux\"] = 3\n",
        "print(sub)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 4,
      "metadata": {
        "pycharm": {}
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "1\n"
          ]
        }
      ],
      "source": [
        "print(sub.expand(\"baz\"))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "pycharm": {}
      },
      "source": [
        "The available attributes and their semantics are documented in the `condor_submit` [manual](https://htcondor.readthedocs.io/en/latest/man-pages/condor_submit.html),\n",
        "sowe won’t repeat them here.\n",
        "A minimal realistic submit object may look like the following:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "metadata": {
        "pycharm": {}
      },
      "outputs": [],
      "source": [
        "sub = htcondor.Submit({\n",
        "    \"executable\": \"/bin/sleep\",\n",
        "    \"arguments\": \"5m\"\n",
        "})"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "pycharm": {}
      },
      "source": [
        "To go from a submit object to job in a schedd, one must use the `submit` method of a `htcondor.Schedd`:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "metadata": {
        "pycharm": {}
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "8\n"
          ]
        }
      ],
      "source": [
        "schedd = htcondor.Schedd()          # create a schedd object connected to the local schedd\n",
        "submit_result = schedd.submit(sub)  # queue one job\n",
        "print(submit_result.cluster())      # print the job's ClusterId"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "pycharm": {}
      },
      "source": [
        "By default, each invocation of `submit` will submit a single job.  A more common use case is to submit many jobs at once - often identical.  Suppose we don't want to submit a single \"sleep\" job, but 10; instead of writing a `for`-loop around the `submit` method, we can use the `count` argument:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "9\n"
          ]
        }
      ],
      "source": [
        "submit_result = schedd.submit(sub, count=10)\n",
        "\n",
        "print(submit_result.cluster())"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "We can now query for those jobs in the queue:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 8,
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "[[ ProcId = 0; EnteredCurrentStatus = 1695159748; ClusterId = 9; JobStatus = 1; ServerTime = 1695159748 ],\n",
              " [ ProcId = 1; EnteredCurrentStatus = 1695159748; ClusterId = 9; JobStatus = 1; ServerTime = 1695159748 ],\n",
              " [ ProcId = 2; EnteredCurrentStatus = 1695159748; ClusterId = 9; JobStatus = 1; ServerTime = 1695159748 ],\n",
              " [ ProcId = 3; EnteredCurrentStatus = 1695159748; ClusterId = 9; JobStatus = 1; ServerTime = 1695159748 ],\n",
              " [ ProcId = 4; EnteredCurrentStatus = 1695159748; ClusterId = 9; JobStatus = 1; ServerTime = 1695159748 ],\n",
              " [ ProcId = 5; EnteredCurrentStatus = 1695159748; ClusterId = 9; JobStatus = 1; ServerTime = 1695159748 ],\n",
              " [ ProcId = 6; EnteredCurrentStatus = 1695159748; ClusterId = 9; JobStatus = 1; ServerTime = 1695159748 ],\n",
              " [ ProcId = 7; EnteredCurrentStatus = 1695159748; ClusterId = 9; JobStatus = 1; ServerTime = 1695159748 ],\n",
              " [ ProcId = 8; EnteredCurrentStatus = 1695159748; ClusterId = 9; JobStatus = 1; ServerTime = 1695159748 ],\n",
              " [ ProcId = 9; EnteredCurrentStatus = 1695159748; ClusterId = 9; JobStatus = 1; ServerTime = 1695159748 ]]"
            ]
          },
          "execution_count": 8,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "schedd.query(\n",
        "    constraint='ClusterId =?= {}'.format(submit_result.cluster()),\n",
        "    projection=[\"ClusterId\", \"ProcId\", \"JobStatus\", \"EnteredCurrentStatus\"],\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "It's not entirely useful to submit many identical jobs -- but rather each one needs to vary slightly based on its ID (the \"process ID\") within the job cluster.  For this, the `Submit` object in Python behaves  similarly to submit files: references within the submit command are evaluated as macros at submit time.\n",
        "\n",
        "For example, suppose we want the argument to `sleep` to vary based on the process ID:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 9,
      "metadata": {},
      "outputs": [],
      "source": [
        "sub = htcondor.Submit({\"executable\": \"/bin/sleep\", \"arguments\": \"$(Process)s\"})"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Here, the `$(Process)` string will be substituted with the process ID at submit time."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "10\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "[[ Args = \"0s\"; ProcId = 0; ClusterId = 10; JobStatus = 1; ServerTime = 1695159748 ],\n",
              " [ Args = \"1s\"; ProcId = 1; ClusterId = 10; JobStatus = 1; ServerTime = 1695159748 ],\n",
              " [ Args = \"2s\"; ProcId = 2; ClusterId = 10; JobStatus = 1; ServerTime = 1695159748 ],\n",
              " [ Args = \"3s\"; ProcId = 3; ClusterId = 10; JobStatus = 1; ServerTime = 1695159748 ],\n",
              " [ Args = \"4s\"; ProcId = 4; ClusterId = 10; JobStatus = 1; ServerTime = 1695159748 ],\n",
              " [ Args = \"5s\"; ProcId = 5; ClusterId = 10; JobStatus = 1; ServerTime = 1695159748 ],\n",
              " [ Args = \"6s\"; ProcId = 6; ClusterId = 10; JobStatus = 1; ServerTime = 1695159748 ],\n",
              " [ Args = \"7s\"; ProcId = 7; ClusterId = 10; JobStatus = 1; ServerTime = 1695159748 ],\n",
              " [ Args = \"8s\"; ProcId = 8; ClusterId = 10; JobStatus = 1; ServerTime = 1695159748 ],\n",
              " [ Args = \"9s\"; ProcId = 9; ClusterId = 10; JobStatus = 1; ServerTime = 1695159748 ]]"
            ]
          },
          "execution_count": 10,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "submit_result = schedd.submit(sub, count=10)\n",
        "\n",
        "print(submit_result.cluster())\n",
        "\n",
        "schedd.query(\n",
        "    constraint='ClusterId=?={}'.format(submit_result.cluster()),\n",
        "    projection=[\"ClusterId\", \"ProcId\", \"JobStatus\", \"Args\"],\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "The macro evaluation behavior (and the various usable tricks and techniques) are identical between the python bindings and the `condor_submit` executable."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "pycharm": {}
      },
      "source": [
        "## Managing Jobs\n",
        "\n",
        "Once a job is in queue, the schedd will try its best to execute it to completion. There are several cases where a user may want to interrupt the normal flow of jobs. Perhaps the results are no longer needed; perhaps the job needs to be edited to correct a submission error. These actions fall under the purview of _job management_.\n",
        "\n",
        "There are two `Schedd` methods dedicated to job management:\n",
        "\n",
        "* `edit()`: Change an attribute for a set of jobs to a given expression. If invoked within a transaction, multiple calls to `edit` are visible atomically.\n",
        "   * The set of jobs to change can be given as a ClassAd expression. If no jobs match the filter, _then an exception is thrown_.\n",
        "* `act()`: Change the state of a job to a given state (remove, hold, suspend, etc).\n",
        "\n",
        "Both methods take a _job specification_: either a ClassAd expression (such as `Owner =?= \"janedoe\"`)\n",
        "or a list of job IDs (such as `[\"1.1\", \"2.2\", \"2.3\"]`).\n",
        "The `act` method takes an argument from the `JobAction` enum.\n",
        "The commonly-used values are:\n",
        "\n",
        "* `Hold`: put a job on hold, vacating a running job if necessary.  A job will stay in the hold\n",
        "   state until explicitly acted upon by the admin or owner.\n",
        "* `Release`: Release a job from the hold state, returning it to Idle.\n",
        "* `Remove`: Remove a job from the Schedd's queue, cleaning it up first on the remote host (if running).\n",
        "   This requires the remote host to acknowledge it has successfully vacated the job, meaning ``Remove`` may\n",
        "   not be instantaneous.\n",
        "* `Vacate`: Cause a running job to be killed on the remote resource and return to idle state.  With\n",
        "  `Vacate`, jobs may be given significant time to cleanly shut down.\n",
        "\n",
        "Here's an example of job management in action:"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "metadata": {
        "pycharm": {}
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[ foo = \"bar\"; ProcId = 0; JobStatus = 1; ServerTime = 1695159748 ]\n",
            "[ foo = \"bar\"; ProcId = 1; JobStatus = 1; ServerTime = 1695159748 ]\n",
            "[ ProcId = 2; JobStatus = 1; ServerTime = 1695159748 ]\n",
            "[ ProcId = 3; JobStatus = 1; ServerTime = 1695159748 ]\n",
            "[ ProcId = 4; JobStatus = 1; ServerTime = 1695159748 ]\n"
          ]
        }
      ],
      "source": [
        "submit_result = schedd.submit(sub, count=5)  # queues 5 copies of this job\n",
        "schedd.edit([f\"{submit_result.cluster()}.{idx}\" for idx in range(2)], \"foo\", '\"bar\"')  # sets attribute foo to the string \"bar\" for the first two jobs\n",
        "    \n",
        "for ad in schedd.query(\n",
        "    constraint=f\"ClusterId == {submit_result.cluster()}\",\n",
        "    projection=[\"ProcId\", \"JobStatus\", \"foo\"],\n",
        "):\n",
        "    print(repr(ad))"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 12,
      "metadata": {
        "pycharm": {}
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[ foo = \"bar\"; ProcId = 0; JobStatus = 1; ServerTime = 1695159748 ]\n",
            "[ foo = \"bar\"; ProcId = 1; JobStatus = 1; ServerTime = 1695159748 ]\n",
            "[ ProcId = 2; JobStatus = 5; ServerTime = 1695159748 ]\n",
            "[ ProcId = 3; JobStatus = 5; ServerTime = 1695159748 ]\n",
            "[ ProcId = 4; JobStatus = 5; ServerTime = 1695159748 ]\n"
          ]
        }
      ],
      "source": [
        "schedd.act(htcondor.JobAction.Hold, f\"ClusterId == {submit_result.cluster()} && ProcId >= 2\")\n",
        "\n",
        "for ad in schedd.query(\n",
        "    constraint=f\"ClusterId == {submit_result.cluster()}\",\n",
        "    projection=[\"ProcId\", \"JobStatus\", \"foo\"],\n",
        "):\n",
        "    print(repr(ad))"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "Finally, let's clean up after ourselves (this will remove all of the jobs you own from the queue)."
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 13,
      "metadata": {
        "jupyter": {
          "outputs_hidden": false
        },
        "pycharm": {
          "name": "#%%\n"
        }
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "[ TotalChangedAds = 1; TotalJobAds = 0; TotalPermissionDenied = 0; TotalAlreadyDone = 0; TotalBadStatus = 0; TotalNotFound = 0; TotalSuccess = 26; TotalError = 0 ]"
            ]
          },
          "execution_count": 13,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "import getpass\n",
        "\n",
        "schedd.act(htcondor.JobAction.Remove, f'Owner == \"{getpass.getuser()}\"')"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "pycharm": {}
      },
      "source": [
        "## That's It!\n",
        "\n",
        "You've made it through the very basics of the Python bindings.  While there are many other features the Python\n",
        "module has to offer, we have covered enough to replace the command line tools of `condor_q`, `condor_submit`,\n",
        "`condor_status`, `condor_rm` and others."
      ]
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3 (ipykernel)",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3.9.6"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 4
}
