From 1222750575bdb906573008add991ea49a8ab540d Mon Sep 17 00:00:00 2001 From: ErinWeisbart Date: Tue, 12 Jul 2022 14:55:28 -0700 Subject: [PATCH 01/10] batches.sh specific for DCP --- files/batches.sh | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 files/batches.sh diff --git a/files/batches.sh b/files/batches.sh deleted file mode 100644 index 200cc00..0000000 --- a/files/batches.sh +++ /dev/null @@ -1,7 +0,0 @@ -# Command to generate batches for a single plate. -# It generates 384*9 tasks, corresponding to 384 wells with 9 images each. -# An image is the unit of parallelization in this example. -# -# You need to install parallel to run this command. - -parallel echo '{\"Metadata\": \"Metadata_Plate={1},Metadata_Well={2}{3},Metadata_Site={4}\"},' ::: Plate1 Plate2 ::: `echo {A..P}` ::: `seq -w 24` ::: `seq -w 9` | sort > batches.txt From 101b1105324eef6db9e25c904b85a1d93b3e2a9c Mon Sep 17 00:00:00 2001 From: ErinWeisbart Date: Tue, 12 Jul 2022 14:59:18 -0700 Subject: [PATCH 02/10] first draft documentation --- documentation/.github/workflows/deploy.yml | 40 ++++++++ .../DS-documentation/SQS_QUEUE_information.md | 41 ++++++++ documentation/DS-documentation/_config.yml | 45 ++++++++ documentation/DS-documentation/_toc.yml | 22 ++++ .../DS-documentation/customizing_DS.md | 6 ++ .../DS-documentation/images/AMIID.jpg | Bin 0 -> 109305 bytes documentation/DS-documentation/images/ECS.jpg | Bin 0 -> 88574 bytes .../DS-documentation/images/InstanceID.jpg | Bin 0 -> 17199 bytes .../DS-documentation/images/Launch.jpg | Bin 0 -> 29903 bytes .../DS-documentation/images/Network.jpg | Bin 0 -> 4573 bytes .../DS-documentation/images/Snapshot.jpg | Bin 0 -> 23294 bytes .../images/sample_DCP_config_1.png | Bin 0 -> 37319 bytes documentation/DS-documentation/overview.md | 95 +++++++++++++++++ documentation/DS-documentation/step_0_prep.md | 96 ++++++++++++++++++ .../DS-documentation/step_1_configuration.md | 84 +++++++++++++++ .../DS-documentation/step_2_submit_jobs.md | 15 +++ .../DS-documentation/step_3_start_cluster.md | 69 +++++++++++++ .../DS-documentation/step_4_monitor.md | 51 ++++++++++ .../DS-documentation/troubleshooting.md | 11 ++ documentation/DS-documentation/versions.md | 10 ++ documentation/README.md | 12 +++ 21 files changed, 597 insertions(+) create mode 100644 documentation/.github/workflows/deploy.yml create mode 100644 documentation/DS-documentation/SQS_QUEUE_information.md create mode 100644 documentation/DS-documentation/_config.yml create mode 100644 documentation/DS-documentation/_toc.yml create mode 100644 documentation/DS-documentation/customizing_DS.md create mode 100644 documentation/DS-documentation/images/AMIID.jpg create mode 100644 documentation/DS-documentation/images/ECS.jpg create mode 100644 documentation/DS-documentation/images/InstanceID.jpg create mode 100644 documentation/DS-documentation/images/Launch.jpg create mode 100644 documentation/DS-documentation/images/Network.jpg create mode 100644 documentation/DS-documentation/images/Snapshot.jpg create mode 100644 documentation/DS-documentation/images/sample_DCP_config_1.png create mode 100644 documentation/DS-documentation/overview.md create mode 100644 documentation/DS-documentation/step_0_prep.md create mode 100644 documentation/DS-documentation/step_1_configuration.md create mode 100644 documentation/DS-documentation/step_2_submit_jobs.md create mode 100644 documentation/DS-documentation/step_3_start_cluster.md create mode 100644 documentation/DS-documentation/step_4_monitor.md create mode 100644 documentation/DS-documentation/troubleshooting.md create mode 100644 documentation/DS-documentation/versions.md create mode 100644 documentation/README.md diff --git a/documentation/.github/workflows/deploy.yml b/documentation/.github/workflows/deploy.yml new file mode 100644 index 0000000..0e64823 --- /dev/null +++ b/documentation/.github/workflows/deploy.yml @@ -0,0 +1,40 @@ +name: deploy + +# Only run this when the master branch changes +on: + push: + branches: + - main + # Only run if edits in DS-documentation + paths: + - documentation/DS-documentation/** + +# This job installs dependencies, builds the book, and pushes it to `gh-pages` +jobs: + deploy-book: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + # Install dependencies + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install dependencies + run: | + pip install jupyter-book + + # Build the book + - name: Build the book + run: | + jupyter-book build DS-documentation/ + + # Push the book's HTML to github-pages + - name: GitHub Pages action + uses: peaceiris/actions-gh-pages@v3.6.1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./DS-documentation/_build/html + cname: distributed-something.github.io diff --git a/documentation/DS-documentation/SQS_QUEUE_information.md b/documentation/DS-documentation/SQS_QUEUE_information.md new file mode 100644 index 0000000..33e9522 --- /dev/null +++ b/documentation/DS-documentation/SQS_QUEUE_information.md @@ -0,0 +1,41 @@ +# SQS QUEUE Information + +This is in-depth information about the configurable components in SQS QUEUE INFORMATION, a section in [Step 1: Configuration](step_1_configuration.md) of running Distributed CellProfiler. + +## SQS_QUEUE_NAME + +**SQS_QUEUE_NAME** is the name of the queue where all of your jobs are sent. (A queue is exactly what it sounds like - a list of things waiting their turn. Jobs represent one complete run through a CellProfiler pipeline (though each job may involve any number of images. e.g. analysis may require thousands of jobs, each with a single image making one complete CellProfiler run, while making an illumination correction may be a single job that iterates through thousands of images to produce a single output file.)) You want a name that is descriptive enough to distinguish it from other queues. We usually name our queues based on the project and the step or pipeline goal. An example may be something like Hepatocyte_Differentiation_Illum or Lipid_Droplet_Analysis. + +## SQS_DEAD_LETTER_QUEUE + +**SQS_DEAD_LETTER_QUEUE** is the name of the queue where all the jobs that failed to run are sent. If everything goes perfectly, this will always remain empty. If jobs that are in the queue fail multiple times (our default is 10) they are moved to the dead-letter queue, which is not used to initiate jobs. The dead-letter queue therefore functions effectively as a log so you can see if any of your jobs failed. It is different from your other queue as machines do not try and pull jobs from it. Protip: Each member of our team maintains their own dead-letter queue so we don’t have to worry about finding messages if multiple people are running jobs at the same time. We use names like DeadMessages_Erin. + +If all of your jobs end up in your dead-letter queue there are many different places you could have a problem. Hopefully, you’ll keep an eye on the logs in your Cloudwatch (the part of AWS used for monitoring what all your other AWS services are doing) after starting a run and catch the issue before all of your jobs fail multiple times. + +If a single job ends up in your dead-letter queue while the rest of your jobs complete successfully, it is likely that that an image is corrupted (a corrupted image is one that has failed to save properly or has been damaged so that it will not open). This is true whether your pipeline processes a single image at a time (such as in analysis runs where you’re interested in cellular measurements on a per-image basis) or whether your pipeline processes many images at a time (such as when making an illumination correction image on a per-plate basis). This is the major reason why we have the dead-letter queue: you certainly don’t want to pay for your cluster to indefinitely attempt to process a corrupted image. Keeping an eye on your Cloudwatch logs wouldn’t necessarily help you catch this kind of error because you could have tens or hundreds of successful jobs run before an instance pulls the job for the corrupted image, or the corrupted image could be thousands of images into an illumination correction run, etc. + +## SQS_MESSAGE_VISIBILITY + +**SQS_MESSAGE_VISIBILITY** controls how long jobs are hidden after being pulled by a machine to run. Jobs must be visible (i.e. not hidden) in order to be pulled by a Docker and therefore run. In other words, the time you enter in SQS_MESSAGE_VISIBILITY is how long a job is allowed a chance to complete before it is unhidden and made available to be started by a different copy of CellProfiler. It’s quite important to set this time correctly- we typically say to estimate 1.5X how long the job typically takes to run (or your best guess of that if you’re not sure). To understand why, and the consequences of setting an incorrect time, let’s look more carefully at the SQS queue. + +The SQS queue has two categories - “Messages Available” and “Messages In Flight”. Each message is a job and regardless of the category it’s in, the jobs all remain in the same queue. In effect, “In Flight” means currently hiding and “Available” means not currently hiding. + +When you submit your Config file to AWS it creates your queue in SQS but that queue starts out empty. When you submit your Jobs file to AWS it puts all of your jobs into the queue under “Messages Available”. When you submit your Fleet file to AWS it 1) creates machines in EC2, 2) ECS puts Docker containers on those instances, and 3) those instances look in “Messages Available” in SQS for jobs to run. + +Once a Docker has pulled a job, that job moves from “Available’ to “In Flight”. It remains hidden (“In Flight”) for the duration of time set in SQS_MESSAGE_VISIBILITY and then it becomes visible again (“Available”). Jobs are hidden so that multiple machines don’t process the same job at the same time. If the job completes successfully, the Docker tells the queue to delete that message. + +If the job completes but it is not successful (e.g. CellProfiler errors), the Docker tells the queue to move the job from “In Flight” to “Available” so another Docker (with a different copy of CellProfiler) can attempt to complete the job. + +If the SQS_MESSAGE_VISIBILITY is too short then a job will become unhidden even though it is still currently being (hopefully successfully) run by the Docker that originally picked it up. This means that another Docker may come along and start the same job and you end up paying for unnecessary compute time because both Dockers will continue to run the job until they each finish. + +If the SQS_MESSAGE_VISIBILITY is too long then you can end up wasting time and money waiting for the job to become available again after a crash even when the rest of your analysis is done. If anything causes a job to stop mid-run (e.g. CellProfiler crashes, the instance crashes, or the instance is removed by AWS because you are outbid), that job stays hidden until the set time. If a Docker instance goes to the queue and doesn’t find any visible jobs, then it does not try to run any more jobs in that copy of CellProfiler, limiting the effective computing power of that Docker. Therefore, some or all of your instances may hang around doing nothing (but costing money) until the job is visible again. When in doubt, it is better to have your SQS_MESSAGE_VISIBILITY set too long than too short because, while crashes can happen, it is rare that AWS takes small machines from your fleet, though we do notice it happening with larger machines. + +There is not an easy way to see if you have selected the appropriate amount of time for your SQS_MESSAGE_VISIBILITY on your first run through a brand new pipeline. To confirm that multiple Dockers didn’t run the same job, after the jobs are complete, you need to manually go through each log in Cloudwatch and figure out how many times you got the word “SUCCESS” in each log. (This may be reasonable to do on an illumination correction run where you have a single job per plate, but it’s not so reasonable if running an analysis pipeline on thousands of individual images). To confirm that multiple Dockers are never processing the same job, you can keep an eye on your queue and make sure that you never have more jobs “In Flight” than the number of copies of CellProfiler that you have running; likewise, if your timeout time is too short, it may seem like too few jobs are “In Flight” even though the CPU usage on all your machines is high. + +Once you have run a pipeline once, you can check the execution time (either by noticing how long after you started your jobs that your first jobs begin to finish, or by checking the logs of individual jobs and noting the start and end time), you will then have an accurate idea of roughly how long that pipeline needs to execute, and can set your message visibility accordingly. You can even do this on the fly while jobs are currently processing; the updated visibility time won’t affect the jobs already out for processing (ie if the time was set to 3 hours and you change it to 1 hour, the jobs already processing will remain hidden for 3 hours or until finished), but any job that begins processing AFTER the change will use the new visibility timeout setting. + +## Example SQS Queue + +[[images/Sample_SQS_Queue.png|alt="Sample_SQS_Queue"]] + +This is an example of an SQS Queue. You can see that there is one active task with 64 jobs in it. In this example, we are running a fleet of 32 instances, each with a single Docker, so at this moment (right after starting the fleet), there are 32 tasks "In Flight" and 32 tasks that are still "Available." You can also see that many lab members have their own dead-letter queues which are, fortunately, all currently empty. diff --git a/documentation/DS-documentation/_config.yml b/documentation/DS-documentation/_config.yml new file mode 100644 index 0000000..e892891 --- /dev/null +++ b/documentation/DS-documentation/_config.yml @@ -0,0 +1,45 @@ +# Book settings +# For your DS implementation, you will need to update author, repository:url:, and html:baseurl: + +# Learn more at https://jupyterbook.org/customize/config.html +title: Documentation +author: Broad Institute +copyright: "2022" +#logo: img/logo.svg + +# Only build files that are in the ToC +only_build_toc_files: true + +# Force re-execution of notebooks on each build. +# See https://jupyterbook.org/content/execute.html +execute: + execute_notebooks: force + +# Define the name of the latex output file for PDF builds +#latex: +# latex_documents: +# targetname: book.tex + +# Add a bibtex file so that we can create citations +# bibtex_bibfiles: +# - references.bib + +# Information about where the book exists on the web +repository: + url: https://github.com/distributed-something/documentation + branch: main # Which branch of the repository should be used when creating links (optional) + path_to_book: DS-documentation + +html: + #favicon: "img/favicon.ico" + baseurl: distributed-something.github.io + use_repository_button: true + use_issues_button: true + use_edit_page_button: true + comments: + hypothesis: true + +parse: + myst_enable_extensions: + # Only required if you use html + - html_image diff --git a/documentation/DS-documentation/_toc.yml b/documentation/DS-documentation/_toc.yml new file mode 100644 index 0000000..9d427ad --- /dev/null +++ b/documentation/DS-documentation/_toc.yml @@ -0,0 +1,22 @@ +# Table of contents +# Learn more at https://jupyterbook.org/customize/toc.html + +format: jb-book +root: overview +parts: +- caption: + chapters: + - file: customizing_DS +- caption: Running Distributed-Something + chapters: + - file: step_0_prep + - file: step_1_configuration + sections: + - file: SQS_QUEUE_information + - file: step_2_submit_jobs + - file: step_3_start_cluster + - file: step_4_monitor +- caption: + chapters: + - file: troubleshooting + - file: versions diff --git a/documentation/DS-documentation/customizing_DS.md b/documentation/DS-documentation/customizing_DS.md new file mode 100644 index 0000000..7856cab --- /dev/null +++ b/documentation/DS-documentation/customizing_DS.md @@ -0,0 +1,6 @@ +# Customizing Distributed-Something + +Distributed-Something is a template. +Examples of implementations can be found at Distributed-CellProfiler, Distributed-Fiji, and Distributed-BioFormats2Raw. +There are many points at which you will need to customize Distributed-Something for your own implementation. +These customization points are documented within each file and summarized below. diff --git a/documentation/DS-documentation/images/AMIID.jpg b/documentation/DS-documentation/images/AMIID.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b7edb8afca546795b8f23c5c7efdd4fe1029512a GIT binary patch literal 109305 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%0ArVOuLs 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 literal 0 HcmV?d00001 diff --git a/documentation/DS-documentation/images/ECS.jpg b/documentation/DS-documentation/images/ECS.jpg new file mode 100644 index 0000000000000000000000000000000000000000..abd90565072b83cbe607cc2b375db25e8fe0972a GIT binary patch literal 88574 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 literal 0 HcmV?d00001 diff --git a/documentation/DS-documentation/images/InstanceID.jpg b/documentation/DS-documentation/images/InstanceID.jpg new file mode 100644 index 0000000000000000000000000000000000000000..925820502a01176b2ed7498a5cc748f2fd77fd8d GIT binary patch literal 17199 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 literal 0 HcmV?d00001 diff --git a/documentation/DS-documentation/images/Launch.jpg b/documentation/DS-documentation/images/Launch.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8686685d9435c8ad25f3955602e26f84c9703fb1 GIT binary patch literal 29903 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 literal 0 HcmV?d00001 diff --git a/documentation/DS-documentation/images/Network.jpg b/documentation/DS-documentation/images/Network.jpg new file mode 100644 index 0000000000000000000000000000000000000000..74ef5fce658e3fc520703c1c374a98eda3ee9be1 GIT binary patch literal 4573 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 FKLAQt#v1?t literal 0 HcmV?d00001 diff --git a/documentation/DS-documentation/images/sample_DCP_config_1.png b/documentation/DS-documentation/images/sample_DCP_config_1.png new file mode 100644 index 0000000000000000000000000000000000000000..4cfc7e12c5fdee3714e3063c8baccbbee23418e4 GIT binary patch literal 37319 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 + sudo apt-get install git + git clone https://github.com/cellprofiler/Distributed-Something.git + cd Distributed-Something/ + git pull + + +#### 2.1.2 Python 3.8 or higher and pip +Most scripts are written in Python and support Python 3.8 and 3.9. +Follow installation instructions for your platform to install python and, if needed, pip. +After Python has been installed, you need to install the requirements for Distributed-Something following this steps: + +
+    cd Distributed-Something/files
+    sudo pip install -r requirements.txt
+
+ +#### 2.1.3 AWS CLI +The command line interface is the main mode of interaction between the local node and the resources in AWS. +You need to install [awscli](http://docs.aws.amazon.com/cli/latest/userguide/installing.html) for Distributed-Something to work properly: + +
+    sudo pip install awscli --ignore-installed six
+    sudo pip install --upgrade awscli
+    aws configure
+
+ +When running the last step, you will need to enter your AWS credentials. +Make sure to set the region correctly (i.e. us-west-1 or eu-east-1, not eu-west-2a), and set the default file type to json. + +#### 2.1.4 s3fs-fuse (optional) +[s3fs-fuse](https://github.com/s3fs-fuse/s3fs-fuse) allows you to mount your s3 bucket as a pseudo-file system. +It does not have all the performance of a real file system, but allows you to easily access all the files in your s3 bucket. +Follow the instructions at the link to mount your bucket. + +#### 2.1.5 Create Control Node AMI (optional) +Once you've set up the other software (and gotten a job running, so you know everything is set up correctly), you can use Amazon's web console to set this up as an Amazon Machine Instance, or AMI, to replicate the current state of the hard drive. +Create future control nodes using this AMI so that you don't need to repeat the above installation. diff --git a/documentation/DS-documentation/step_1_configuration.md b/documentation/DS-documentation/step_1_configuration.md new file mode 100644 index 0000000..ba370cd --- /dev/null +++ b/documentation/DS-documentation/step_1_configuration.md @@ -0,0 +1,84 @@ +# Step 1: Configuration + +The first step in setting up any job is editing the values in the config.py file. +Once the config file is created, simply type `python run.py setup` to set up your resources based on the configurations you've specified. + +*** + +## Components of the config file + +* **APP_NAME:** This will be used to tie your clusters, tasks, services, logs, and alarms together. +It need not be unique, but it should be descriptive enough that you can tell jobs apart if you're running multiple analyses (i.e. "NuclearSegmentation_Drosophila" is better than "CellProfiler"). + +*** + +* **DOCKERHUB_TAG:** This is the encapsulated version of your software your analyses will be running. + +*** + +### AWS GENERAL SETTINGS +These are settings that will allow your instances to be configured correctly and access the resources they need- see [Step 0: Prep](step_0_prep.md) for more information. + +*** + +### EC2 AND ECS INFORMATION + +* **ECS_CLUSTER:** Which ECS cluster you'd like the jobs to go into. +All AWS accounts come with a "default" cluster, but you may add more clusters if you like. +Distinct clusters for each job are not necessary, but if you're running multiple analyses at once it can help avoid the wrong Docker containers (such as the ones for your "NuclearSegmentation_Drosophila" job) going to the wrong instances (such as the instances that are part of your "NuclearSegmentation_HeLa" spot fleet). +* **CLUSTER_MACHINES:** How many EC2 instances you want to have in your cluster. +* **TASKS_PER_MACHINE:** How many Docker containers to place on each machine. +* **MACHINE_TYPE:** A list of what type(s) of machines your spot fleet should contain. +* **MACHINE_PRICE:** How much you're willing to pay per hour for each machine launched. +AWS has a handy [price history tracker](https://console.aws.amazon.com/ec2sp/v1/spot/home) you can use to make a reasonable estimate of how much to bid. +If your jobs complete quickly and/or you don't need the data immediately you can reduce your bid accordingly; jobs that may take many hours to finish or that you need results from immediately may justify a higher bid. +* **EBS_VOL_SIZE:** The size of the temporary hard drive associated with each EC2 instance in GB. +The minimum allowed is 22. +If you have multiple Dockers running per machine, each Docker will have access to (EBS_VOL_SIZE/TASKS_PER_MACHINE)- 2 GB of space. + +*** + +### DOCKER INSTANCE RUNNING ENVIRONMENT +* **DOCKER_CORES:** How many copies of your script to run in each Docker container. +* **CPU_SHARES:** How many CPUs each Docker container may have. +* **MEMORY:** How much memory each Docker container may have. +* **SECONDS_TO_START:** The time each Docker core will wait before it starts another copy of your software. +This can safely be set to 0 for workflows that don't require much memory or execute quickly; for slower and/or more memory intensive pipelines we advise you to space them out by roughly the length of your most memory intensive step to make sure your software doesn't crash due to lack of memory. + +*** + +### SQS QUEUE INFORMATION + +* **SQS_QUEUE_NAME:** The name of the queue where all of your jobs will be sent. +* **SQS_MESSAGE_VISIBILITY:** How long each job is hidden from view before being allowed to be tried again. +We recommend setting this to slightly longer than the average amount of time it takes an individual job to process- if you set it too short, you may waste resources doing the same job multiple times; if you set it too long, your instances may have to wait around a long while to access a job that was sent to an instance that stalled or has since been terminated. +* **SQS_DEAD_LETTER_QUEUE:** The name of the queue to send jobs to if they fail to process correctly multiple times; this keeps a single bad job (such as one where a single file has been corrupted) from keeping your cluster active indefinitely. +See [[Setting up|Before-you-get-started:-setting-up]] for more information. + +*** + +### LOG GROUP INFORMATION + +* **LOG_GROUP_NAME:** The name to give the log group that will monitor the progress of your jobs and allow you to check performance or look for problems after the fact. + +*** + +### REDUNDANCY CHECKS + +If an analysis fails partway through (due to some of the files being in the wrong place, an AWS outage, a machine crash, etc.), setting this to 'True' this allows you to resubmit the whole analysis but only reprocess jobs that haven't already been done. +This saves you from having to try to parse exactly which jobs succeeded vs failed or from having to pay to rerun the entire analysis. +If your software determines the correct number of files are already in the output folder it will designate that job as completed and move onto the next one. + +If you actually do want to overwrite files that were previously generated (such as when you have improved a pipeline and no longer want the output of the old version), set this to 'False' to process jobs whether or not there are already files in the output folder. + +* **CHECK_IF_DONE_BOOL:** Whether or not to check the output folder before proceeding. +Case-insensitive. +* **EXPECTED_NUMBER_FILES:** How many files need to be in the output folder in order to mark a job as completed. +* **MIN_FILE_SIZE_BYTES:** What is the minimal number of bytes an object should be to "count"? +Useful when trying to detect jobs that may have exported smaller corrupted files vs larger, full-size files. +* **NECESSARY_STRING:** This allows you to optionally set a string that must be included in your file to count towards the total in EXPECTED_NUMBER_FILES. + +*** + +### YOUR CONFIGURATIONS +* **VARIABLE:** Add in any additional system variables specific to your program. diff --git a/documentation/DS-documentation/step_2_submit_jobs.md b/documentation/DS-documentation/step_2_submit_jobs.md new file mode 100644 index 0000000..c33c64d --- /dev/null +++ b/documentation/DS-documentation/step_2_submit_jobs.md @@ -0,0 +1,15 @@ +# Step 2: Submit Jobs + +Distributed-Something works by breaking your workflow into a series of smaller jobs based on the metadata and groupings you've specified in your job file. +The choice of how to group your jobs is largely dependent on the details of your workflow. +Once you've decided on a grouping, you're ready to start configuring your job file. +Once your job file is configured, simply use `python run.py submitJob files/{YourJobFile}.json` to send all the jobs to the SQS queue [[specified in your config file|Step-1:--Configuration]]. + +## Configuring your job file + +All keys (outside of your groups) are shared between all jobs. +Common keys include **input_location**, **output_location**, **script_to_use**. + +* **groups:** The list of all the groups you'd like to process. +Keys within each job can either be used to define the job (e.g. Metadata, file location) or can be used to pass job-specific variables. +For large numbers of groups, it may be helpful to create this list separately as a txt file you can then append into the jobs JSON file using your favorite scripting language. diff --git a/documentation/DS-documentation/step_3_start_cluster.md b/documentation/DS-documentation/step_3_start_cluster.md new file mode 100644 index 0000000..b8a7663 --- /dev/null +++ b/documentation/DS-documentation/step_3_start_cluster.md @@ -0,0 +1,69 @@ +# Step 3: Start Cluster + +After your jobs have been submitted to the queue, it is time to start your cluster. +Once you have configured your spot fleet request per the instructions below, you may run +`python run.py startCluster files/{YourFleetFile}.json` + +When you enter this command, the following things will happen (in this order): + +* Your spot fleet request will be sent to AWS. +Depending on their capacity and the price that you bid, it can take anywhere from a couple of minutes to several hours for your machines to be ready. +* Distributed-Something will create the APP_NAMESpotFleetRequestId.json file, which will allow you to [start your progress monitor](step_4_monitor.md). +This will allow you to walk away and just let things run even if your spot fleet won't be ready for some time. + +Once the spot fleet is ready: + +* Distributed-Something will create the log groups (if they don't already exist) for your log streams to go in. +* Distributed-Something will ask AWS to place Docker containers onto the instances in your spot fleet. +Your job will begin shortly! + +*** +## Configuring your spot fleet request +Definition of many of these terms and explanations of many of the individual configuration parameters of spot fleets are covered in AWS documentation [here](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-fleet.html) and [here](http://docs.aws.amazon.com/cli/latest/reference/ec2/request-spot-fleet.html). +You may also configure your spot fleet request through Amazon's web interface and simply download the JSON file at the "review" page to generate the configuration file you want, though we do not recommend this as Distributed-Something assumes a certain fleet request structure and has only been tested on certain Amazon AMI's. +Looking at the output of this automatically generated spot fleet request can be useful though for obtaining values like your VPC's subnet and security groups, as well the ARN ID's of your roles. + +Among the parameters you should/must update: + +* **The IamFleetRole, IamInstanceProfile, KeyName, SubnetId, and Groups:** These are account specific and you will have configure these based on the [previous setup work that you did](step_0_prep.md). +Once you've created your first complete spot fleet request, you can save a copy as a local template so that you don't have to look these up every time. + + * The KeyName used here should be the same used in your config file but **without** the `.pem` extension. + +* **ImageId and SnapshotId** These refer to the OS and pre-installed programming that will be used by your spot fleet instances, and are both AWS region specific. +We use the Amazon ECS-Optimized Amazon Linux AMI; but the Linux 2 AMI also seems to work in our limited testing. +If there is no template fleet file for your region, or the one here is too out-of-date, see below for instructions on configuring these options yourselves. +If you have a good working configuration for a region that isn't represented or for a more up-to-date version of the AMI than we've had time to test, please feel free to create a pull request and we'll include it in the repo! + +## To run in a region where a spot fleet config isn't available or is out of date: + +* Under EC2 -> Instances select "Launch Instance" + +![Launch Instance](images/Launch.jpg) + +* Search "ECS", then choose the "Amazon ECS-Optimized Amazon Linux AMI" + +![Select ECS-Optimized](images/ECS.jpg) + +* Select Continue, then select any instance type (we're going to kill this after a few seconds) and click "Next: Configure Instance Details" + +* Choose a network and subnet in the region you wish to launch instances in, and then click "Next: Add Storage" + +![Set Network and Subnet](images/Network.jpg) + +* On the "Add Storage" page, note down the Snapshot column for the Root volume- this is your SnapshotId. +Click "Review and Launch" + +![Get SnapshotId](images/Snapshot.jpg) + +* Click "Launch", and then select any key pair (again, we'll be killing this in a few seconds) + +* Once your instance has launched, click its link from the launch page. + +![Click InstanceID](images/InstanceID.jpg) + +* In the list of information on the instance, find and note its AMI ID - this is your ImageId + +![Get the AMI ID](images/AMIID.jpg) + +* Terminate the instance diff --git a/documentation/DS-documentation/step_4_monitor.md b/documentation/DS-documentation/step_4_monitor.md new file mode 100644 index 0000000..7e9bbe1 --- /dev/null +++ b/documentation/DS-documentation/step_4_monitor.md @@ -0,0 +1,51 @@ +# Step 4: Monitor + +Your workflow is now submitted. +Distributed-Something will keep an eye on a few things for you at this point without you having to do anything else. + +* Each instance is labeled with your APP_NAME, so that you can easily find your instances if you want to look at the instance metrics on the Running Instances section of the [EC2 web interface](https://console.aws.amazon.com/ec2/v2/home) to monitor performance. + +* You can also look at the whole-cluster CPU and memory usage statistics related to your APP_NAME in the [ECS web interface](https://console.aws.amazon.com/ecs/home). + +* Each instance will have an alarm placed on it so that if CPU usage to dips below 1% for 15 consecutive minutes (almost always the result of a crashed machine), the instance will be automatically terminated and a new one will take its place. + +* Each individual job processed will create a log of the CellProfiler output, and each Docker container will create a log showing CPU, memory, and disk usage. + +If you choose to run the monitor script, Distributed-Something can be even more helpful. +The monitor can be run by entering `python run.py monitor files/APP_NAMESpotFleetRequestId.json`; the JSON file containing all the information Distributed-Something needs will have been automatically created when you sent the instructions to start your cluster in the previous step. + +(**Note:** You should run the monitor inside [Screen](https://www.gnu.org/software/screen/), [tmux](https://tmux.github.io/), or another comparable service to keep a network disconnection from killing your monitor; this is particularly critical the longer your run takes.) + +*** + +## Monitor functions + +### While your analysis is running + +* Checks your queue once per minute to see how many jobs are currently processing and how many remain to be processed. + +* Once per day, it deletes the alarms for any instances that have been terminated in the last 24 hours (because of spot prices rising above your maximum bid, machine crashes, etc). + +### When the number of jobs in your queue goes to 0 + +* Downscales the ECS service associated with your APP_NAME. + +* Deletes all the alarms associated with your spot fleet (both the currently running and the previously terminated instances). + +* Shuts down your spot fleet to keep you from incurring charges after your analysis is over. + +* Gets rid of the queue, service, and task definition created for this analysis. + +* Exports all the logs from your analysis onto your S3 bucket. + +*** + +## Cheapest mode + +You can run the monitor in an optional "cheapest" mode, which will downscale the number of requested machines (but not RUNNING machines) to one 15 minutes after the monitor is engaged. +You can engage cheapest mode by adding `True` as a final configurable parameter when starting the monitor, aka `python run.py monitor files/APP_NAMESpotFleetRequestId.json True` + +Cheapest mode is cheapest because it will remove all but 1 machine as soon as that machine crashes and/or runs out of jobs to do; this can save you money, particularly in multi-CPU Dockers running long jobs. + +This mode is optional though, because running this way involves some inherent risks- if machines stall out due to processing errors, they will not be replaced, meaning your job will take overall longer. +Additionally, if there is limited capacity for your requested configuration when you first start (aka you want 200 machines but AWS says it can currently only allocate you 50), more machines will not be added if and when they become available in cheapest mode as they would in normal mode. diff --git a/documentation/DS-documentation/troubleshooting.md b/documentation/DS-documentation/troubleshooting.md new file mode 100644 index 0000000..4cee473 --- /dev/null +++ b/documentation/DS-documentation/troubleshooting.md @@ -0,0 +1,11 @@ +# Troubleshooting + +We recommend you create a troubleshooting table that describes common failure modes for your implementation of Distributed-Something. +Shown below are errors common to most implementations of DS. +Distributed-CellProfiler documentation has a [thorough example](https://github.com/CellProfiler/Distributed-CellProfiler/wiki/Troubleshooting). + +| SQS | Cloudwatch | S3 | EC2/ECS | Problem | Solution | +|---|---|---|---|---|---| +| | Within a single log, your run command is logging multiple times. | Expected output seen. | | A single job is being processed multiple times. | SQS_MESSAGE_VISIBILITY is set too short. See SQS-QUEUE-INFORMATION for more information. | +| | Your specified output structure does not match the Metadata passed. | Expected output is seen. | | This is not necessarily an error. If the input grouping is different than the output grouping (e.g. jobs are run by Plate-Well-Site but are all output to a single Plate folder) then this will print in the Cloudwatch log that matches the input structure but actual job progress will print in the Cloudwatch log that matches the output structure. | | +| | | | Machines made in EC2 and dockers are made in ECS but the dockers are not placed on the machines. | There is a mismatch in your DS config file. | Confirm that the MEMORY matches the MACHINE_TYPE set in your config. | diff --git a/documentation/DS-documentation/versions.md b/documentation/DS-documentation/versions.md new file mode 100644 index 0000000..58137d5 --- /dev/null +++ b/documentation/DS-documentation/versions.md @@ -0,0 +1,10 @@ +# Versions + +The most current release can always be found at `cellprofiler/distributed-something`. +Current version is 1.0.0. + +--- + +# Version History + +## 1.0.0 - Version as of 20220701 diff --git a/documentation/README.md b/documentation/README.md new file mode 100644 index 0000000..0e89c69 --- /dev/null +++ b/documentation/README.md @@ -0,0 +1,12 @@ +Read more at +Make for your use case +Make sure you update documentation to fit your particular Distributed-Something implementation. + +Will auto generate for you at URL every time you push to main. +Can manually generate with + +Points at which documentation will need to be updated for your particular implementation: +- config.yml + - author + - repository:url: + - html:baseurl: From 819f451770fa87496e02a9eb75e7a0782f1cf79f Mon Sep 17 00:00:00 2001 From: ErinWeisbart Date: Wed, 13 Jul 2022 14:52:14 -0700 Subject: [PATCH 03/10] cleanup --- config.py | 18 +- run.py | 673 +++++++++++++++++++++---------------- worker/Dockerfile | 13 +- worker/generic-worker.py | 242 +++++++------ worker/instance-monitor.py | 12 +- worker/run-worker.sh | 8 +- 6 files changed, 539 insertions(+), 427 deletions(-) diff --git a/config.py b/config.py index 9d3f99b..957f39d 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,6 @@ # Constants (User configurable) -APP_NAME = 'DistributedSomething' # Used to generate derivative names unique to the application. +APP_NAME = 'DistributedSomething' # Used to generate derivative names unique to the application. # DOCKER REGISTRY INFORMATION: DOCKERHUB_TAG = 'user/distributed-something:sometag' @@ -20,10 +20,10 @@ EBS_VOL_SIZE = 30 # In GB. Minimum allowed is 22. # DOCKER INSTANCE RUNNING ENVIRONMENT: -DOCKER_CORES = 4 # Number of CellProfiler processes to run inside a docker container +DOCKER_CORES = 4 # Number of sofrware processes to run inside a docker container CPU_SHARES = DOCKER_CORES * 1024 # ECS computing units assigned to each docker container (1024 units = 1 core) -MEMORY = 15000 # Memory assigned to the docker container in MB -SECONDS_TO_START = 3*60 # Wait before the next CP process is initiated to avoid memory collisions +MEMORY = 15000 # Memory assigned to the docker container in MB +SECONDS_TO_START = 3*60 # Wait before the next process is initiated to avoid memory collisions # SQS QUEUE INFORMATION: SQS_QUEUE_NAME = APP_NAME + 'Queue' @@ -31,12 +31,12 @@ SQS_DEAD_LETTER_QUEUE = 'arn:aws:sqs:some-region:111111100000:DeadMessages' # LOG GROUP INFORMATION: -LOG_GROUP_NAME = APP_NAME +LOG_GROUP_NAME = APP_NAME # REDUNDANCY CHECKS -CHECK_IF_DONE_BOOL = 'False' #True or False- should it check if there are a certain number of non-empty files and delete the job if yes? -EXPECTED_NUMBER_FILES = 7 #What is the number of files that trigger skipping a job? -MIN_FILE_SIZE_BYTES = 1 #What is the minimal number of bytes an object should be to "count"? -NECESSARY_STRING = '' #Is there any string that should be in the file name to "count"? +CHECK_IF_DONE_BOOL = 'False' # True or False - should it check if there are a certain number of non-empty files and delete the job if yes? +EXPECTED_NUMBER_FILES = 7 # What is the number of files that trigger skipping a job? +MIN_FILE_SIZE_BYTES = 1 # What is the minimal number of bytes an object should be to "count"? +NECESSARY_STRING = '' # Is there any string that should be in the file name to "count"? # PUT ANYTHING SPECIFIC TO YOUR PROGRAM DOWN HERE diff --git a/run.py b/run.py index 32dc36f..52d36f0 100644 --- a/run.py +++ b/run.py @@ -1,4 +1,3 @@ -from __future__ import print_function import os, sys import boto3 import datetime @@ -10,6 +9,7 @@ from email.mime.text import MIMEText from config import * + WAIT_TIME = 60 MONITOR_TIME = 60 @@ -22,12 +22,7 @@ "family": APP_NAME, "containerDefinitions": [ { - "environment": [ - { - "name": "AWS_REGION", - "value": AWS_REGION - } - ], + "environment": [{"name": "AWS_REGION", "value": AWS_REGION}], "name": APP_NAME, "image": DOCKERHUB_TAG, "cpu": CPU_SHARES, @@ -37,13 +32,13 @@ "logConfiguration": { "logDriver": "awslogs", "options": { - "awslogs-group": LOG_GROUP_NAME+"_perInstance", + "awslogs-group": LOG_GROUP_NAME + "_perInstance", "awslogs-region": AWS_REGION, - "awslogs-stream-prefix": APP_NAME - } - } + "awslogs-stream-prefix": APP_NAME, + }, + }, } - ] + ], } SQS_DEFINITION = { @@ -51,8 +46,10 @@ "MaximumMessageSize": "262144", "MessageRetentionPeriod": "1209600", "ReceiveMessageWaitTimeSeconds": "0", - "RedrivePolicy": "{\"deadLetterTargetArn\":\"" + SQS_DEAD_LETTER_QUEUE + "\",\"maxReceiveCount\":\"10\"}", - "VisibilityTimeout": str(SQS_MESSAGE_VISIBILITY) + "RedrivePolicy": '{"deadLetterTargetArn":"' + + SQS_DEAD_LETTER_QUEUE + + '","maxReceiveCount":"10"}', + "VisibilityTimeout": str(SQS_MESSAGE_VISIBILITY), } @@ -60,256 +57,285 @@ # AUXILIARY FUNCTIONS ################################# + def get_aws_credentials(AWS_PROFILE): session = boto3.Session(profile_name=AWS_PROFILE) credentials = session.get_credentials() return credentials.access_key, credentials.secret_key + def generate_task_definition(AWS_PROFILE): task_definition = TASK_DEFINITION.copy() key, secret = get_aws_credentials(AWS_PROFILE) - sqs = boto3.client('sqs') + sqs = boto3.client("sqs") queue_name = get_queue_url(sqs) - task_definition['containerDefinitions'][0]['environment'] += [ - { - 'name': 'APP_NAME', - 'value': APP_NAME - }, - { - 'name': 'SQS_QUEUE_URL', - 'value': queue_name - }, - { - "name": "AWS_ACCESS_KEY_ID", - "value": key - }, - { - "name": "AWS_SECRET_ACCESS_KEY", - "value": secret - }, - { - "name": "AWS_BUCKET", - "value": AWS_BUCKET - }, - { - "name": "DOCKER_CORES", - "value": str(DOCKER_CORES) - }, - { - "name": "LOG_GROUP_NAME", - "value": LOG_GROUP_NAME - }, - { - "name": "CHECK_IF_DONE_BOOL", - "value": CHECK_IF_DONE_BOOL - }, - { - "name": "EXPECTED_NUMBER_FILES", - "value": str(EXPECTED_NUMBER_FILES) - }, - { - "name": "ECS_CLUSTER", - "value": ECS_CLUSTER - }, - { - "name": "SECONDS_TO_START", - "value": str(SECONDS_TO_START) - }, - { - "name": "MIN_FILE_SIZE_BYTES", - "value": str(MIN_FILE_SIZE_BYTES) - }, - { - "name": "NECESSARY_STRING", - "value": NECESSARY_STRING - } + task_definition["containerDefinitions"][0]["environment"] += [ + {"name": "APP_NAME", "value": APP_NAME}, + {"name": "SQS_QUEUE_URL", "value": queue_name}, + {"name": "AWS_ACCESS_KEY_ID", "value": key}, + {"name": "AWS_SECRET_ACCESS_KEY", "value": secret}, + {"name": "AWS_BUCKET", "value": AWS_BUCKET}, + {"name": "DOCKER_CORES", "value": str(DOCKER_CORES)}, + {"name": "LOG_GROUP_NAME", "value": LOG_GROUP_NAME}, + {"name": "CHECK_IF_DONE_BOOL", "value": CHECK_IF_DONE_BOOL}, + {"name": "EXPECTED_NUMBER_FILES", "value": str(EXPECTED_NUMBER_FILES)}, + {"name": "ECS_CLUSTER", "value": ECS_CLUSTER}, + {"name": "SECONDS_TO_START", "value": str(SECONDS_TO_START)}, + {"name": "MIN_FILE_SIZE_BYTES", "value": str(MIN_FILE_SIZE_BYTES)}, + {"name": "NECESSARY_STRING", "value": NECESSARY_STRING}, ] return task_definition + def update_ecs_task_definition(ecs, ECS_TASK_NAME, AWS_PROFILE): task_definition = generate_task_definition(AWS_PROFILE) - ecs.register_task_definition(family=ECS_TASK_NAME,containerDefinitions=task_definition['containerDefinitions']) - print('Task definition registered') + ecs.register_task_definition( + family=ECS_TASK_NAME, + containerDefinitions=task_definition["containerDefinitions"], + ) + print("Task definition registered") + def get_or_create_cluster(ecs): data = ecs.list_clusters() - cluster = [clu for clu in data['clusterArns'] if clu.endswith(ECS_CLUSTER)] + cluster = [clu for clu in data["clusterArns"] if clu.endswith(ECS_CLUSTER)] if len(cluster) == 0: ecs.create_cluster(clusterName=ECS_CLUSTER) time.sleep(WAIT_TIME) - print('Cluster '+ECS_CLUSTER+' created') + print("Cluster " + ECS_CLUSTER + " created") else: - print('Cluster '+ECS_CLUSTER+' exists') + print("Cluster " + ECS_CLUSTER + " exists") + def create_or_update_ecs_service(ecs, ECS_SERVICE_NAME, ECS_TASK_NAME): # Create the service with no workers (0 desired count) data = ecs.list_services(cluster=ECS_CLUSTER) - service = [srv for srv in data['serviceArns'] if srv.endswith(ECS_SERVICE_NAME)] + service = [srv for srv in data["serviceArns"] if srv.endswith(ECS_SERVICE_NAME)] if len(service) > 0: - print('Service exists. Removing') + print("Service exists. Removing") ecs.delete_service(cluster=ECS_CLUSTER, service=ECS_SERVICE_NAME) - print('Removed service '+ECS_SERVICE_NAME) + print("Removed service " + ECS_SERVICE_NAME) time.sleep(WAIT_TIME) - print('Creating new service') - ecs.create_service(cluster=ECS_CLUSTER, serviceName=ECS_SERVICE_NAME, taskDefinition=ECS_TASK_NAME, desiredCount=0) - print('Service created') + print("Creating new service") + ecs.create_service( + cluster=ECS_CLUSTER, + serviceName=ECS_SERVICE_NAME, + taskDefinition=ECS_TASK_NAME, + desiredCount=0, + ) + print("Service created") + def get_queue_url(sqs): result = sqs.list_queues() - if 'QueueUrls' in result.keys(): - for u in result['QueueUrls']: - if u.split('/')[-1] == SQS_QUEUE_NAME: + if "QueueUrls" in result.keys(): + for u in result["QueueUrls"]: + if u.split("/")[-1] == SQS_QUEUE_NAME: return u return None + def get_or_create_queue(sqs): u = get_queue_url(sqs) if u is None: - print('Creating queue') + print("Creating queue") sqs.create_queue(QueueName=SQS_QUEUE_NAME, Attributes=SQS_DEFINITION) time.sleep(WAIT_TIME) else: - print('Queue exists') + print("Queue exists") + def loadConfig(configFile): data = None - with open(configFile, 'r') as conf: + with open(configFile, "r") as conf: data = json.load(conf) return data -def killdeadAlarms(fleetId,monitorapp,ec2,cloud): - todel=[] - changes = ec2.describe_spot_fleet_request_history(SpotFleetRequestId=fleetId,StartTime=(datetime.datetime.now()-datetime.timedelta(hours=2)).replace(microsecond=0)) - for eachevent in changes['HistoryRecords']: - if eachevent['EventType']=='instanceChange': - if eachevent['EventInformation']['EventSubType']=='terminated': - todel.append(eachevent['EventInformation']['InstanceId']) - existing_alarms = [x['AlarmName'] for x in cloud.describe_alarms(AlarmNamePrefix=monitorapp)['MetricAlarms']] - +def killdeadAlarms(fleetId, monitorapp, ec2, cloud): + todel = [] + changes = ec2.describe_spot_fleet_request_history( + SpotFleetRequestId=fleetId, + StartTime=(datetime.datetime.now() - datetime.timedelta(hours=2)).replace( + microsecond=0 + ), + ) + for eachevent in changes["HistoryRecords"]: + if eachevent["EventType"] == "instanceChange": + if eachevent["EventInformation"]["EventSubType"] == "terminated": + todel.append(eachevent["EventInformation"]["InstanceId"]) + + existing_alarms = [ + x["AlarmName"] + for x in cloud.describe_alarms(AlarmNamePrefix=monitorapp)["MetricAlarms"] + ] + for eachmachine in todel: - monitorname = monitorapp+'_'+eachmachine + monitorname = monitorapp + "_" + eachmachine if monitorname in existing_alarms: cloud.delete_alarms(AlarmNames=[monitorname]) - print('Deleted', monitorname, 'if it existed') + print("Deleted", monitorname, "if it existed") time.sleep(3) - - print('Old alarms deleted') - -def generateECSconfig(ECS_CLUSTER,APP_NAME,AWS_BUCKET,s3client): - configfile=open('configtemp.config','w') - configfile.write('ECS_CLUSTER='+ECS_CLUSTER+'\n') - configfile.write('ECS_AVAILABLE_LOGGING_DRIVERS=["json-file","awslogs"]') - configfile.close() - s3client.upload_file('configtemp.config',AWS_BUCKET,'ecsconfigs/'+APP_NAME+'_ecs.config') - os.remove('configtemp.config') - return 's3://'+AWS_BUCKET+'/ecsconfigs/'+APP_NAME+'_ecs.config' -def generateUserData(ecsConfigFile,dockerBaseSize): - config_str = '#!/bin/bash \n' - config_str += 'sudo yum install -y aws-cli \n' - config_str += 'sudo yum install -y awslogs \n' - config_str += 'aws s3 cp '+ecsConfigFile+' /etc/ecs/ecs.config' + print("Old alarms deleted") - boothook_str = '#!/bin/bash \n' - boothook_str += "echo 'OPTIONS="+'"${OPTIONS} --storage-opt dm.basesize='+str(dockerBaseSize)+'G"'+"' >> /etc/sysconfig/docker" - config = MIMEText(config_str, _subtype='x-shellscript') - config.add_header('Content-Disposition', 'attachment',filename='config_temp.txt') - - boothook = MIMEText(boothook_str, _subtype='cloud-boothook') - boothook.add_header('Content-Disposition', 'attachment',filename='boothook_temp.txt') +def generateECSconfig(ECS_CLUSTER, APP_NAME, AWS_BUCKET, s3client): + configfile = open("configtemp.config", "w") + configfile.write("ECS_CLUSTER=" + ECS_CLUSTER + "\n") + configfile.write('ECS_AVAILABLE_LOGGING_DRIVERS=["json-file","awslogs"]') + configfile.close() + s3client.upload_file( + "configtemp.config", AWS_BUCKET, "ecsconfigs/" + APP_NAME + "_ecs.config" + ) + os.remove("configtemp.config") + return "s3://" + AWS_BUCKET + "/ecsconfigs/" + APP_NAME + "_ecs.config" + + +def generateUserData(ecsConfigFile, dockerBaseSize): + config_str = "#!/bin/bash \n" + config_str += "sudo yum install -y aws-cli \n" + config_str += "sudo yum install -y awslogs \n" + config_str += "aws s3 cp " + ecsConfigFile + " /etc/ecs/ecs.config" + + boothook_str = "#!/bin/bash \n" + boothook_str += ( + "echo 'OPTIONS=" + + '"${OPTIONS} --storage-opt dm.basesize=' + + str(dockerBaseSize) + + 'G"' + + "' >> /etc/sysconfig/docker" + ) + + config = MIMEText(config_str, _subtype="x-shellscript") + config.add_header("Content-Disposition", "attachment", filename="config_temp.txt") + + boothook = MIMEText(boothook_str, _subtype="cloud-boothook") + boothook.add_header( + "Content-Disposition", "attachment", filename="boothook_temp.txt" + ) pre_user_data = MIMEMultipart() pre_user_data.attach(boothook) pre_user_data.attach(config) - try: #Python2 + try: # Python2 return b64encode(pre_user_data.as_string()) - except TypeError: #Python3 + except TypeError: # Python3 pre_user_data_string = pre_user_data.as_string() - return b64encode(pre_user_data_string.encode('utf-8')).decode('utf-8') + return b64encode(pre_user_data_string.encode("utf-8")).decode("utf-8") + def removequeue(queueName): - sqs = boto3.client('sqs') - queueoutput= sqs.list_queues(QueueNamePrefix=queueName) - if len(queueoutput["QueueUrls"])==1: - queueUrl=queueoutput["QueueUrls"][0] - else: #In case we have "AnalysisQueue" and "AnalysisQueue1" and only want to delete the first of those + sqs = boto3.client("sqs") + queueoutput = sqs.list_queues(QueueNamePrefix=queueName) + if len(queueoutput["QueueUrls"]) == 1: + queueUrl = queueoutput["QueueUrls"][0] + else: # In case we have "AnalysisQueue" and "AnalysisQueue1" and only want to delete the first of those for eachUrl in queueoutput["QueueUrls"]: - if eachUrl.split('/')[-1] == queueName: - queueUrl=eachUrl - + if eachUrl.split("/")[-1] == queueName: + queueUrl = eachUrl + sqs.delete_queue(QueueUrl=queueUrl) + def deregistertask(taskName, ecs): - taskArns = ecs.list_task_definitions(familyPrefix=taskName, status='ACTIVE') - for eachtask in taskArns['taskDefinitionArns']: - fulltaskname=eachtask.split('/')[-1] + taskArns = ecs.list_task_definitions(familyPrefix=taskName, status="ACTIVE") + for eachtask in taskArns["taskDefinitionArns"]: + fulltaskname = eachtask.split("/")[-1] ecs.deregister_task_definition(taskDefinition=fulltaskname) + def removeClusterIfUnused(clusterName, ecs): - if clusterName != 'default': - #never delete the default cluster + if clusterName != "default": + # never delete the default cluster result = ecs.describe_clusters(clusters=[clusterName]) - if sum([result['clusters'][0]['pendingTasksCount'],result['clusters'][0]['runningTasksCount'],result['clusters'][0]['activeServicesCount'],result['clusters'][0]['registeredContainerInstancesCount']])==0: + if ( + sum( + [ + result["clusters"][0]["pendingTasksCount"], + result["clusters"][0]["runningTasksCount"], + result["clusters"][0]["activeServicesCount"], + result["clusters"][0]["registeredContainerInstancesCount"], + ] + ) + == 0 + ): ecs.delete_cluster(cluster=clusterName) + def downscaleSpotFleet(queue, spotFleetID, ec2, manual=False): visible, nonvisible = queue.returnLoad() if manual: - ec2.modify_spot_fleet_request(ExcessCapacityTerminationPolicy='noTermination', SpotFleetRequestId=spotFleetID, TargetCapacity = int(manual)) + ec2.modify_spot_fleet_request( + ExcessCapacityTerminationPolicy="noTermination", + SpotFleetRequestId=spotFleetID, + TargetCapacity=int(manual), + ) return elif visible > 0: return else: status = ec2.describe_spot_fleet_instances(SpotFleetRequestId=spotFleetID) - if nonvisible < len(status['ActiveInstances']): - ec2.modify_spot_fleet_request(ExcessCapacityTerminationPolicy='noTermination', SpotFleetRequestId=spotFleetID, TargetCapacity = nonvisible) + if nonvisible < len(status["ActiveInstances"]): + ec2.modify_spot_fleet_request( + ExcessCapacityTerminationPolicy="noTermination", + SpotFleetRequestId=spotFleetID, + TargetCapacity=nonvisible, + ) + def export_logs(logs, loggroupId, starttime, bucketId): - result = logs.create_export_task(taskName = loggroupId, logGroupName = loggroupId, fromTime = int(starttime), to = int(time.time()*1000), destination = bucketId, destinationPrefix = 'exportedlogs/'+loggroupId) - - logExportId = result['taskId'] + result = logs.create_export_task( + taskName=loggroupId, + logGroupName=loggroupId, + fromTime=int(starttime), + to=int(time.time() * 1000), + destination=bucketId, + destinationPrefix="exportedlogs/" + loggroupId, + ) + + logExportId = result["taskId"] while True: - result = logs.describe_export_tasks(taskId = logExportId) - if result['exportTasks'][0]['status']['code']!='PENDING': - if result['exportTasks'][0]['status']['code']!='RUNNING': - print(result['exportTasks'][0]['status']['code']) + result = logs.describe_export_tasks(taskId=logExportId) + if result["exportTasks"][0]["status"]["code"] != "PENDING": + if result["exportTasks"][0]["status"]["code"] != "RUNNING": + print(result["exportTasks"][0]["status"]["code"]) break time.sleep(30) + ################################# # CLASS TO HANDLE SQS QUEUE ################################# -class JobQueue(): - def __init__(self,name=None): - self.sqs = boto3.resource('sqs') - if name==None: +class JobQueue: + def __init__(self, name=None): + self.sqs = boto3.resource("sqs") + if name == None: self.queue = self.sqs.get_queue_by_name(QueueName=SQS_QUEUE_NAME) else: self.queue = self.sqs.get_queue_by_name(QueueName=name) - self.inProcess = -1 + self.inProcess = -1 self.pending = -1 def scheduleBatch(self, data): msg = json.dumps(data) response = self.queue.send_message(MessageBody=msg) - print('Batch sent. Message ID:',response.get('MessageId')) + print("Batch sent. Message ID:", response.get("MessageId")) def pendingLoad(self): self.queue.load() - visible = int( self.queue.attributes['ApproximateNumberOfMessages'] ) - nonVis = int( self.queue.attributes['ApproximateNumberOfMessagesNotVisible'] ) - if [visible, nonVis] != [self.pending,self.inProcess]: + visible = int(self.queue.attributes["ApproximateNumberOfMessages"]) + nonVis = int(self.queue.attributes["ApproximateNumberOfMessagesNotVisible"]) + if [visible, nonVis] != [self.pending, self.inProcess]: self.pending = visible self.inProcess = nonVis d = datetime.datetime.now() - print(d,'In process:',nonVis,'Pending',visible) + print(d, "In process:", nonVis, "Pending", visible) if visible + nonVis > 0: return True else: @@ -317,8 +343,8 @@ def pendingLoad(self): def returnLoad(self): self.queue.load() - visible = int( self.queue.attributes['ApproximateNumberOfMessages'] ) - nonVis = int( self.queue.attributes['ApproximateNumberOfMessagesNotVisible'] ) + visible = int(self.queue.attributes["ApproximateNumberOfMessages"]) + nonVis = int(self.queue.attributes["ApproximateNumberOfMessagesNotVisible"]) return visible, nonVis @@ -326,163 +352,211 @@ def returnLoad(self): # SERVICE 1: SETUP (formerly fab) ################################# + def setup(): - ECS_TASK_NAME = APP_NAME + 'Task' - ECS_SERVICE_NAME = APP_NAME + 'Service' - USER = os.environ['HOME'].split('/')[-1] - AWS_CONFIG_FILE_NAME = os.environ['HOME'] + '/.aws/config' - AWS_CREDENTIAL_FILE_NAME = os.environ['HOME'] + '/.aws/credentials' - sqs = boto3.client('sqs') + ECS_TASK_NAME = APP_NAME + "Task" + ECS_SERVICE_NAME = APP_NAME + "Service" + USER = os.environ["HOME"].split("/")[-1] + AWS_CONFIG_FILE_NAME = os.environ["HOME"] + "/.aws/config" + AWS_CREDENTIAL_FILE_NAME = os.environ["HOME"] + "/.aws/credentials" + sqs = boto3.client("sqs") get_or_create_queue(sqs) - ecs = boto3.client('ecs') + ecs = boto3.client("ecs") get_or_create_cluster(ecs) update_ecs_task_definition(ecs, ECS_TASK_NAME, AWS_PROFILE) create_or_update_ecs_service(ecs, ECS_SERVICE_NAME, ECS_TASK_NAME) + ################################# # SERVICE 2: SUBMIT JOB ################################# + def submitJob(): if len(sys.argv) < 3: - print('Use: run.py submitJob jobfile') + print("Use: run.py submitJob jobfile") sys.exit() # Step 1: Read the job configuration file jobInfo = loadConfig(sys.argv[2]) - templateMessage = {eachkey:jobInfo[eachkey] for eachkey in jobInfo.keys() if eachkey != "groups" and "_comment" not in eachkey} + templateMessage = { + eachkey: jobInfo[eachkey] + for eachkey in jobInfo.keys() + if eachkey != "groups" and "_comment" not in eachkey + } # Step 2: Reach the queue and schedule tasks - print('Contacting queue') + print("Contacting queue") queue = JobQueue() - print('Scheduling tasks') + print("Scheduling tasks") for batch in jobInfo["groups"]: templateMessage["group"] = batch queue.scheduleBatch(templateMessage) - print('Job submitted. Check your queue') + print("Job submitted. Check your queue") + ################################# -# SERVICE 3: START CLUSTER +# SERVICE 3: START CLUSTER ################################# + def startCluster(): if len(sys.argv) < 3: - print('Use: run.py startCluster configFile') + print("Use: run.py startCluster configFile") sys.exit() thistime = datetime.datetime.now().replace(microsecond=0) - #Step 1: set up the configuration files - s3client = boto3.client('s3') - ecsConfigFile=generateECSconfig(ECS_CLUSTER,APP_NAME,AWS_BUCKET,s3client) - spotfleetConfig=loadConfig(sys.argv[2]) - spotfleetConfig['ValidFrom']=thistime - spotfleetConfig['ValidUntil']=(thistime+datetime.timedelta(days=365)).replace(microsecond=0) - spotfleetConfig['TargetCapacity']= CLUSTER_MACHINES - spotfleetConfig['SpotPrice'] = '%.2f' %MACHINE_PRICE - DOCKER_BASE_SIZE = int(round(float(EBS_VOL_SIZE)/int(TASKS_PER_MACHINE))) - 2 - userData=generateUserData(ecsConfigFile,DOCKER_BASE_SIZE) - for LaunchSpecification in range(0,len(spotfleetConfig['LaunchSpecifications'])): - spotfleetConfig['LaunchSpecifications'][LaunchSpecification]["UserData"]=userData - spotfleetConfig['LaunchSpecifications'][LaunchSpecification]['BlockDeviceMappings'][1]['Ebs']["VolumeSize"]= EBS_VOL_SIZE - spotfleetConfig['LaunchSpecifications'][LaunchSpecification]['InstanceType'] = MACHINE_TYPE[LaunchSpecification] - + # Step 1: set up the configuration files + s3client = boto3.client("s3") + ecsConfigFile = generateECSconfig(ECS_CLUSTER, APP_NAME, AWS_BUCKET, s3client) + spotfleetConfig = loadConfig(sys.argv[2]) + spotfleetConfig["ValidFrom"] = thistime + spotfleetConfig["ValidUntil"] = (thistime + datetime.timedelta(days=365)).replace( + microsecond=0 + ) + spotfleetConfig["TargetCapacity"] = CLUSTER_MACHINES + spotfleetConfig["SpotPrice"] = "%.2f" % MACHINE_PRICE + DOCKER_BASE_SIZE = int(round(float(EBS_VOL_SIZE) / int(TASKS_PER_MACHINE))) - 2 + userData = generateUserData(ecsConfigFile, DOCKER_BASE_SIZE) + for LaunchSpecification in range(0, len(spotfleetConfig["LaunchSpecifications"])): + spotfleetConfig["LaunchSpecifications"][LaunchSpecification][ + "UserData" + ] = userData + spotfleetConfig["LaunchSpecifications"][LaunchSpecification][ + "BlockDeviceMappings" + ][1]["Ebs"]["VolumeSize"] = EBS_VOL_SIZE + spotfleetConfig["LaunchSpecifications"][LaunchSpecification][ + "InstanceType" + ] = MACHINE_TYPE[LaunchSpecification] # Step 2: make the spot fleet request - ec2client=boto3.client('ec2') + ec2client = boto3.client("ec2") requestInfo = ec2client.request_spot_fleet(SpotFleetRequestConfig=spotfleetConfig) - print('Request in process. Wait until your machines are available in the cluster.') - print('SpotFleetRequestId',requestInfo['SpotFleetRequestId']) + print("Request in process. Wait until your machines are available in the cluster.") + print("SpotFleetRequestId", requestInfo["SpotFleetRequestId"]) # Step 3: Make the monitor - starttime=str(int(time.time()*1000)) - createMonitor=open('files/' + APP_NAME + 'SpotFleetRequestId.json','w') - createMonitor.write('{"MONITOR_FLEET_ID" : "'+requestInfo['SpotFleetRequestId']+'",\n') - createMonitor.write('"MONITOR_APP_NAME" : "'+APP_NAME+'",\n') - createMonitor.write('"MONITOR_ECS_CLUSTER" : "'+ECS_CLUSTER+'",\n') - createMonitor.write('"MONITOR_QUEUE_NAME" : "'+SQS_QUEUE_NAME+'",\n') - createMonitor.write('"MONITOR_BUCKET_NAME" : "'+AWS_BUCKET+'",\n') - createMonitor.write('"MONITOR_LOG_GROUP_NAME" : "'+LOG_GROUP_NAME+'",\n') - createMonitor.write('"MONITOR_START_TIME" : "'+ starttime+'"}\n') + starttime = str(int(time.time() * 1000)) + createMonitor = open("files/" + APP_NAME + "SpotFleetRequestId.json", "w") + createMonitor.write( + '{"MONITOR_FLEET_ID" : "' + requestInfo["SpotFleetRequestId"] + '",\n' + ) + createMonitor.write('"MONITOR_APP_NAME" : "' + APP_NAME + '",\n') + createMonitor.write('"MONITOR_ECS_CLUSTER" : "' + ECS_CLUSTER + '",\n') + createMonitor.write('"MONITOR_QUEUE_NAME" : "' + SQS_QUEUE_NAME + '",\n') + createMonitor.write('"MONITOR_BUCKET_NAME" : "' + AWS_BUCKET + '",\n') + createMonitor.write('"MONITOR_LOG_GROUP_NAME" : "' + LOG_GROUP_NAME + '",\n') + createMonitor.write('"MONITOR_START_TIME" : "' + starttime + '"}\n') createMonitor.close() - + # Step 4: Create a log group for this app and date if one does not already exist - logclient=boto3.client('logs') - loggroupinfo=logclient.describe_log_groups(logGroupNamePrefix=LOG_GROUP_NAME) - groupnames=[d['logGroupName'] for d in loggroupinfo['logGroups']] + logclient = boto3.client("logs") + loggroupinfo = logclient.describe_log_groups(logGroupNamePrefix=LOG_GROUP_NAME) + groupnames = [d["logGroupName"] for d in loggroupinfo["logGroups"]] if LOG_GROUP_NAME not in groupnames: logclient.create_log_group(logGroupName=LOG_GROUP_NAME) logclient.put_retention_policy(logGroupName=LOG_GROUP_NAME, retentionInDays=60) - if LOG_GROUP_NAME+'_perInstance' not in groupnames: - logclient.create_log_group(logGroupName=LOG_GROUP_NAME+'_perInstance') - logclient.put_retention_policy(logGroupName=LOG_GROUP_NAME+'_perInstance', retentionInDays=60) - + if LOG_GROUP_NAME + "_perInstance" not in groupnames: + logclient.create_log_group(logGroupName=LOG_GROUP_NAME + "_perInstance") + logclient.put_retention_policy( + logGroupName=LOG_GROUP_NAME + "_perInstance", retentionInDays=60 + ) + # Step 5: update the ECS service to be ready to inject docker containers in EC2 instances - print('Updating service') - ecs = boto3.client('ecs') - ecs.update_service(cluster=ECS_CLUSTER, service=APP_NAME+'Service', desiredCount=CLUSTER_MACHINES*TASKS_PER_MACHINE) - print('Service updated.') - + print("Updating service") + ecs = boto3.client("ecs") + ecs.update_service( + cluster=ECS_CLUSTER, + service=APP_NAME + "Service", + desiredCount=CLUSTER_MACHINES * TASKS_PER_MACHINE, + ) + print("Service updated.") + # Step 6: Monitor the creation of the instances until all are present - status = ec2client.describe_spot_fleet_instances(SpotFleetRequestId=requestInfo['SpotFleetRequestId']) - #time.sleep(15) # This is now too fast, so sometimes the spot fleet request history throws an error! - while len(status['ActiveInstances']) < CLUSTER_MACHINES: + status = ec2client.describe_spot_fleet_instances( + SpotFleetRequestId=requestInfo["SpotFleetRequestId"] + ) + # time.sleep(15) # This is now too fast, so sometimes the spot fleet request history throws an error! + while len(status["ActiveInstances"]) < CLUSTER_MACHINES: # First check to make sure there's not a problem - errorcheck = ec2client.describe_spot_fleet_request_history(SpotFleetRequestId=requestInfo['SpotFleetRequestId'], EventType='error', StartTime=thistime - datetime.timedelta(minutes=1)) - if len(errorcheck['HistoryRecords']) != 0: - print('Your spot fleet request is causing an error and is now being cancelled. Please check your configuration and try again') - for eacherror in errorcheck['HistoryRecords']: - print(eacherror['EventInformation']['EventSubType'] + ' : ' + eacherror['EventInformation']['EventDescription']) - #If there's only one error, and it's the type we see for insufficient capacity (but also other types) - #AND if there are some machines on, indicating that other than capacity the spec is otherwise good, don't cancel - if len(errorcheck['HistoryRecords']) == 1: - if errorcheck['HistoryRecords'][0]['EventInformation']['EventSubType'] == 'allLaunchSpecsTemporarilyBlacklisted': - if len(status['ActiveInstances']) >= 1: - print("I think, but am not sure, that this is an insufficient capacity error. You should check the console for more information.") + errorcheck = ec2client.describe_spot_fleet_request_history( + SpotFleetRequestId=requestInfo["SpotFleetRequestId"], + EventType="error", + StartTime=thistime - datetime.timedelta(minutes=1), + ) + if len(errorcheck["HistoryRecords"]) != 0: + print( + "Your spot fleet request is causing an error and is now being cancelled. Please check your configuration and try again" + ) + for eacherror in errorcheck["HistoryRecords"]: + print( + eacherror["EventInformation"]["EventSubType"] + + " : " + + eacherror["EventInformation"]["EventDescription"] + ) + # If there's only one error, and it's the type we see for insufficient capacity (but also other types) + # AND if there are some machines on, indicating that other than capacity the spec is otherwise good, don't cancel + if len(errorcheck["HistoryRecords"]) == 1: + if ( + errorcheck["HistoryRecords"][0]["EventInformation"]["EventSubType"] + == "allLaunchSpecsTemporarilyBlacklisted" + ): + if len(status["ActiveInstances"]) >= 1: + print( + "I think, but am not sure, that this is an insufficient capacity error. You should check the console for more information." + ) return - ec2client.cancel_spot_fleet_requests(SpotFleetRequestIds=[requestInfo['SpotFleetRequestId']], TerminateInstances=True) + ec2client.cancel_spot_fleet_requests( + SpotFleetRequestIds=[requestInfo["SpotFleetRequestId"]], + TerminateInstances=True, + ) return - + # If everything seems good, just bide your time until you're ready to go - print('.') + print(".") time.sleep(20) - status = ec2client.describe_spot_fleet_instances(SpotFleetRequestId=requestInfo['SpotFleetRequestId']) + status = ec2client.describe_spot_fleet_instances( + SpotFleetRequestId=requestInfo["SpotFleetRequestId"] + ) + + print("Spot fleet successfully created. Your job should start in a few minutes.") - print('Spot fleet successfully created. Your job should start in a few minutes.') ################################# -# SERVICE 4: MONITOR JOB +# SERVICE 4: MONITOR JOB ################################# + def monitor(cheapest=False): if len(sys.argv) < 3: - print('Use: run.py monitor spotFleetIdFile') + print("Use: run.py monitor spotFleetIdFile") sys.exit() - - if '.json' not in sys.argv[2]: - print('Use: run.py monitor spotFleetIdFile') + + if ".json" not in sys.argv[2]: + print("Use: run.py monitor spotFleetIdFile") sys.exit() if len(sys.argv) == 4: cheapest = sys.argv[3] - + monitorInfo = loadConfig(sys.argv[2]) - monitorcluster=monitorInfo["MONITOR_ECS_CLUSTER"] - monitorapp=monitorInfo["MONITOR_APP_NAME"] - fleetId=monitorInfo["MONITOR_FLEET_ID"] - queueId=monitorInfo["MONITOR_QUEUE_NAME"] + monitorcluster = monitorInfo["MONITOR_ECS_CLUSTER"] + monitorapp = monitorInfo["MONITOR_APP_NAME"] + fleetId = monitorInfo["MONITOR_FLEET_ID"] + queueId = monitorInfo["MONITOR_QUEUE_NAME"] - ec2 = boto3.client('ec2') - cloud = boto3.client('cloudwatch') + ec2 = boto3.client("ec2") + cloud = boto3.client("cloudwatch") # Optional Step 0 - decide if you're going to be cheap rather than fast. This means that you'll get 15 minutes # from the start of the monitor to get as many machines as you get, and then it will set the requested number to 1. - # Benefit: this will always be the cheapest possible way to run, because if machines die they'll die fast, - # Potential downside- if machines are at low availability when you start to run, you'll only ever get a small number + # Benefit: this will always be the cheapest possible way to run, because if machines die they'll die fast, + # Potential downside- if machines are at low availability when you start to run, you'll only ever get a small number # of machines (as opposed to getting more later when they become available), so it might take VERY long to run if that happens. if cheapest: queue = JobQueue(name=queueId) startcountdown = time.time() - while queue.pendingLoad(): + while queue.pendingLoad(): if time.time() - startcountdown > 900: downscaleSpotFleet(queue, fleetId, ec2, manual=1) break @@ -491,96 +565,101 @@ def monitor(cheapest=False): # Step 1: Create job and count messages periodically queue = JobQueue(name=queueId) while queue.pendingLoad(): - #Once an hour (except at midnight) check for terminated machines and delete their alarms. - #This is slooooooow, which is why we don't just do it at the end - curtime=datetime.datetime.now().strftime('%H%M') - if curtime[-2:]=='00': - if curtime[:2]!='00': - killdeadAlarms(fleetId,monitorapp,ec2,cloud) - #Once every 10 minutes, check if all jobs are in process, and if so scale the spot fleet size to match - #the number of jobs still in process WITHOUT force terminating them. - #This can help keep costs down if, for example, you start up 100+ machines to run a large job, and - #1-10 jobs with errors are keeping it rattling around for hours. - if curtime[-1:]=='9': + # Once an hour (except at midnight) check for terminated machines and delete their alarms. + # This is slooooooow, which is why we don't just do it at the end + curtime = datetime.datetime.now().strftime("%H%M") + if curtime[-2:] == "00": + if curtime[:2] != "00": + killdeadAlarms(fleetId, monitorapp, ec2, cloud) + # Once every 10 minutes, check if all jobs are in process, and if so scale the spot fleet size to match + # the number of jobs still in process WITHOUT force terminating them. + # This can help keep costs down if, for example, you start up 100+ machines to run a large job, and + # 1-10 jobs with errors are keeping it rattling around for hours. + if curtime[-1:] == "9": downscaleSpotFleet(queue, fleetId, ec2) time.sleep(MONITOR_TIME) - + # Step 2: When no messages are pending, stop service # Reload the monitor info, because for long jobs new fleets may have been started, etc monitorInfo = loadConfig(sys.argv[2]) - monitorcluster=monitorInfo["MONITOR_ECS_CLUSTER"] - monitorapp=monitorInfo["MONITOR_APP_NAME"] - fleetId=monitorInfo["MONITOR_FLEET_ID"] - queueId=monitorInfo["MONITOR_QUEUE_NAME"] - bucketId=monitorInfo["MONITOR_BUCKET_NAME"] - loggroupId=monitorInfo["MONITOR_LOG_GROUP_NAME"] - starttime=monitorInfo["MONITOR_START_TIME"] - - ecs = boto3.client('ecs') - ecs.update_service(cluster=monitorcluster, service=monitorapp+'Service', desiredCount=0) - print('Service has been downscaled') + monitorcluster = monitorInfo["MONITOR_ECS_CLUSTER"] + monitorapp = monitorInfo["MONITOR_APP_NAME"] + fleetId = monitorInfo["MONITOR_FLEET_ID"] + queueId = monitorInfo["MONITOR_QUEUE_NAME"] + bucketId = monitorInfo["MONITOR_BUCKET_NAME"] + loggroupId = monitorInfo["MONITOR_LOG_GROUP_NAME"] + starttime = monitorInfo["MONITOR_START_TIME"] + + ecs = boto3.client("ecs") + ecs.update_service( + cluster=monitorcluster, service=monitorapp + "Service", desiredCount=0 + ) + print("Service has been downscaled") # Step3: Delete the alarms from active machines and machines that have died since the last sweep # This is in a try loop, because while it is important, we don't want to not stop the spot fleet try: result = ec2.describe_spot_fleet_instances(SpotFleetRequestId=fleetId) - instancelist = result['ActiveInstances'] + instancelist = result["ActiveInstances"] while len(instancelist) > 0: to_del = instancelist[:100] - del_alarms = [monitorapp+'_'+x['InstanceId'] for x in to_del] + del_alarms = [monitorapp + "_" + x["InstanceId"] for x in to_del] cloud.delete_alarms(AlarmNames=del_alarms) time.sleep(10) instancelist = instancelist[100:] - killdeadAlarms(fleetId,monitorapp) + killdeadAlarms(fleetId, monitorapp) except: pass # Step 4: Read spot fleet id and terminate all EC2 instances - print('Shutting down spot fleet',fleetId) - ec2.cancel_spot_fleet_requests(SpotFleetRequestIds=[fleetId], TerminateInstances=True) - print('Job done.') + print("Shutting down spot fleet", fleetId) + ec2.cancel_spot_fleet_requests( + SpotFleetRequestIds=[fleetId], TerminateInstances=True + ) + print("Job done.") # Step 5. Release other resources # Remove SQS queue, ECS Task Definition, ECS Service - ECS_TASK_NAME = monitorapp + 'Task' - ECS_SERVICE_NAME = monitorapp + 'Service' - print('Deleting existing queue.') + ECS_TASK_NAME = monitorapp + "Task" + ECS_SERVICE_NAME = monitorapp + "Service" + print("Deleting existing queue.") removequeue(queueId) - print('Deleting service') - ecs.delete_service(cluster=monitorcluster, service = ECS_SERVICE_NAME) - print('De-registering task') - deregistertask(ECS_TASK_NAME,ecs) + print("Deleting service") + ecs.delete_service(cluster=monitorcluster, service=ECS_SERVICE_NAME) + print("De-registering task") + deregistertask(ECS_TASK_NAME, ecs) print("Removing cluster if it's not the default and not otherwise in use") removeClusterIfUnused(monitorcluster, ecs) - #Step 6: Export the logs to S3 - logs=boto3.client('logs') + # Step 6: Export the logs to S3 + logs = boto3.client("logs") - print('Transfer of program logs to S3 initiated') + print("Transfer of program logs to S3 initiated") export_logs(logs, loggroupId, starttime, bucketId) - print('Transfer of per-instance logs to S3 initiated') - export_logs(logs, loggroupId+'_perInstance', starttime, bucketId) + print("Transfer of per-instance logs to S3 initiated") + export_logs(logs, loggroupId + "_perInstance", starttime, bucketId) + + print("All export tasks done") - print('All export tasks done') ################################# -# MAIN USER INTERACTION +# MAIN USER INTERACTION ################################# -if __name__ == '__main__': +if __name__ == "__main__": if len(sys.argv) < 2: - print('Use: run.py setup | submitJob | startCluster | monitor') + print("Use: run.py setup | submitJob | startCluster | monitor") sys.exit() - - if sys.argv[1] == 'setup': + + if sys.argv[1] == "setup": setup() - elif sys.argv[1] == 'submitJob': + elif sys.argv[1] == "submitJob": submitJob() - elif sys.argv[1] == 'startCluster': + elif sys.argv[1] == "startCluster": startCluster() - elif sys.argv[1] == 'monitor': + elif sys.argv[1] == "monitor": monitor() else: - print('Use: run.py setup | submitJob | startCluster | monitor') + print("Use: run.py setup | submitJob | startCluster | monitor") sys.exit() diff --git a/worker/Dockerfile b/worker/Dockerfile index da3ab4e..4728ae4 100644 --- a/worker/Dockerfile +++ b/worker/Dockerfile @@ -2,13 +2,13 @@ # - [ BROAD'16 ] - # # A docker instance for accessing AWS resources -# This wraps the cellprofiler docker registry +# This wraps your custom docker registry # FROM someuser/somedocker:sometag -# Install S3FS + RUN apt-get -y update && \ apt-get -y upgrade && \ @@ -25,6 +25,7 @@ RUN apt-get -y update && \ sysstat \ curl +# Install S3FS WORKDIR /usr/local/src RUN git clone https://github.com/s3fs-fuse/s3fs-fuse.git WORKDIR /usr/local/src/s3fs-fuse @@ -35,23 +36,18 @@ RUN make install # Install Python - not needed if you've already got it in your container # If you have a non-3.8 version, you will need to change python3.8 calls where specified - RUN apt install -y python3.8-dev python3.8-distutils # Install AWS CLI - -RUN python3.8 -m pip install awscli +RUN python3.8 -m pip install awscli # Install boto3 - RUN python3.8 -m pip install boto3 # Install watchtower for logging - RUN python3.8 -m pip install watchtower # SETUP NEW ENTRYPOINT - RUN mkdir -p /home/ubuntu/ WORKDIR /home/ubuntu COPY generic-worker.py . @@ -62,4 +58,3 @@ RUN chmod 755 run-worker.sh WORKDIR /home/ubuntu ENTRYPOINT ["./run-worker.sh"] CMD [""] - diff --git a/worker/generic-worker.py b/worker/generic-worker.py index 63706f7..b4d7c7f 100644 --- a/worker/generic-worker.py +++ b/worker/generic-worker.py @@ -1,62 +1,60 @@ -from __future__ import print_function import boto3 -import glob import json import logging import os -import re import subprocess -import sys import time import watchtower -import string ################################# # CONSTANT PATHS IN THE CONTAINER ################################# -DATA_ROOT = '/home/ubuntu/bucket' -LOCAL_OUTPUT = '/home/ubuntu/local_output' -QUEUE_URL = os.environ['SQS_QUEUE_URL'] -AWS_BUCKET = os.environ['AWS_BUCKET'] -LOG_GROUP_NAME= os.environ['LOG_GROUP_NAME'] -CHECK_IF_DONE_BOOL= os.environ['CHECK_IF_DONE_BOOL'] -EXPECTED_NUMBER_FILES= os.environ['EXPECTED_NUMBER_FILES'] -if 'MIN_FILE_SIZE_BYTES' not in os.environ: +DATA_ROOT = "/home/ubuntu/bucket" +LOCAL_OUTPUT = "/home/ubuntu/local_output" +QUEUE_URL = os.environ["SQS_QUEUE_URL"] +AWS_BUCKET = os.environ["AWS_BUCKET"] +LOG_GROUP_NAME = os.environ["LOG_GROUP_NAME"] +CHECK_IF_DONE_BOOL = os.environ["CHECK_IF_DONE_BOOL"] +EXPECTED_NUMBER_FILES = os.environ["EXPECTED_NUMBER_FILES"] +if "MIN_FILE_SIZE_BYTES" not in os.environ: MIN_FILE_SIZE_BYTES = 1 else: - MIN_FILE_SIZE_BYTES = int(os.environ['MIN_FILE_SIZE_BYTES']) -if 'USE_PLUGINS' not in os.environ: - USE_PLUGINS = 'False' + MIN_FILE_SIZE_BYTES = int(os.environ["MIN_FILE_SIZE_BYTES"]) +if "USE_PLUGINS" not in os.environ: + USE_PLUGINS = "False" else: - USE_PLUGINS = os.environ['USE_PLUGINS'] -if 'NECESSARY_STRING' not in os.environ: + USE_PLUGINS = os.environ["USE_PLUGINS"] +if "NECESSARY_STRING" not in os.environ: NECESSARY_STRING = False else: - NECESSARY_STRING = os.environ['NECESSARY_STRING'] -if 'DOWNLOAD_FILES' not in os.environ: + NECESSARY_STRING = os.environ["NECESSARY_STRING"] +if "DOWNLOAD_FILES" not in os.environ: DOWNLOAD_FILES = False else: - DOWNLOAD_FILES = os.environ['DOWNLOAD_FILES'] + DOWNLOAD_FILES = os.environ["DOWNLOAD_FILES"] +# If you added more system variables to config.py, enter them here -localIn = '/home/ubuntu/local_input' +localIn = "/home/ubuntu/local_input" ################################# # CLASS TO HANDLE THE SQS QUEUE ################################# -class JobQueue(): +class JobQueue: def __init__(self, queueURL): - self.client = boto3.client('sqs') + self.client = boto3.client("sqs") self.queueURL = queueURL - + def readMessage(self): - response = self.client.receive_message(QueueUrl=self.queueURL, WaitTimeSeconds=20) - if 'Messages' in response.keys(): - data = json.loads(response['Messages'][0]['Body']) - handle = response['Messages'][0]['ReceiptHandle'] + response = self.client.receive_message( + QueueUrl=self.queueURL, WaitTimeSeconds=20 + ) + if "Messages" in response.keys(): + data = json.loads(response["Messages"][0]["Body"]) + handle = response["Messages"][0]["ReceiptHandle"] return data, handle else: return None, None @@ -66,130 +64,168 @@ def deleteMessage(self, handle): return def returnMessage(self, handle): - self.client.change_message_visibility(QueueUrl=self.queueURL, ReceiptHandle=handle, VisibilityTimeout=60) + self.client.change_message_visibility( + QueueUrl=self.queueURL, ReceiptHandle=handle, VisibilityTimeout=60 + ) return + ################################# # AUXILIARY FUNCTIONS ################################# -def monitorAndLog(process,logger): +def monitorAndLog(process, logger): while True: - output= process.stdout.readline().decode() - if output== '' and process.poll() is not None: + output = process.stdout.readline().decode() + if output == "" and process.poll() is not None: break if output: print(output.strip()) - logger.info(output) + logger.info(output) + -def printandlog(text,logger): +def printandlog(text, logger): print(text) logger.info(text) + ################################# # RUN SOME PROCESS ################################# + def runSomething(message): - #List the directories in the bucket- this prevents a strange s3fs error - rootlist=os.listdir(DATA_ROOT) + # List the directories in the bucket- this prevents a strange S3Fs error + # You can remove this if you are not mounting S3FS + rootlist = os.listdir(DATA_ROOT) for eachSubDir in rootlist: - subDirName=os.path.join(DATA_ROOT,eachSubDir) + subDirName = os.path.join(DATA_ROOT, eachSubDir) if os.path.isdir(subDirName): - trashvar=os.system('ls '+subDirName) + trashvar = os.system("ls " + subDirName) # Configure the logs logger = logging.getLogger(__name__) - # Parse your message somehow to pull out a name variable that's going to make sense to you when you want to look at the logs later - # What's commented out below will work, otherwise, create your own - #group_to_run = message["group"] - #groupkeys = list(group_to_run.keys()) - #groupkeys.sort() - #metadataID = '-'.join(groupkeys) - - # Add a handler with - # watchtowerlogger=watchtower.CloudWatchLogHandler(log_group=LOG_GROUP_NAME, stream_name=str(metadataID),create_log_group=False) - # logger.addHandler(watchtowerlogger) - - # See if this is a message you've already handled, if you've so chosen - # First, build a variable called remoteOut that equals your unique prefix of where your output should be + # Parse your message to pull out a name variable to use for logging + # To include all group keys in your log name, use the commented-out code below + # Otherwise, create your own definition of metadataID + # group_to_run = message["group"] + # groupkeys = list(group_to_run.keys()) + # groupkeys.sort() + # metadataID = '-'.join(groupkeys) + + # Add a handler with + watchtowerlogger = watchtower.CloudWatchLogHandler( + log_group=LOG_GROUP_NAME, stream_name=str(metadataID), create_log_group=False + ) + logger.addHandler(watchtowerlogger) + + # See if this is a message you've already handled, if you've so chosen + # First, build a variable called remoteOut that equals your unique prefix of where your output should be + # e.g remoteOut = os.path.join(message['output'], metadataID) + # Then check if there are too many files - - if CHECK_IF_DONE_BOOL.upper() == 'TRUE': + if CHECK_IF_DONE_BOOL.upper() == "TRUE": try: - s3client=boto3.client('s3') - bucketlist=s3client.list_objects(Bucket=AWS_BUCKET,Prefix=remoteOut+'/') - objectsizelist=[k['Size'] for k in bucketlist['Contents']] + s3client = boto3.client("s3") + bucketlist = s3client.list_objects( + Bucket=AWS_BUCKET, Prefix=remoteOut + "/" + ) + objectsizelist = [k["Size"] for k in bucketlist["Contents"]] objectsizelist = [i for i in objectsizelist if i >= MIN_FILE_SIZE_BYTES] if NECESSARY_STRING: - if NECESSARY_STRING != '': - objectsizelist = [i for i in objectsizelist if NECESSARY_STRING in i] - if len(objectsizelist)>=int(EXPECTED_NUMBER_FILES): - printandlog('File not run due to > expected number of files',logger) + if NECESSARY_STRING != "": + objectsizelist = [ + i for i in objectsizelist if NECESSARY_STRING in i + ] + if len(objectsizelist) >= int(EXPECTED_NUMBER_FILES): + printandlog("File not run due to > expected number of files", logger) logger.removeHandler(watchtowerlogger) - return 'SUCCESS' - except KeyError: #Returned if that folder does not exist - pass + return "SUCCESS" + except KeyError: # Returned if that folder does not exist + pass + + # If you need to download files locally, perform that step here + # printandlog("Downloading files", logger) # Build and run your program's command - # ie cmd = my-program --my-flag-1 True --my-flag-2 VARIABLE - # you should assign the variable "localOut" to the output location where you expect your program to put files + # e.g. cmd = my-program --my-flag-1 True --my-flag-2 VARIABLE - print('Running', cmd) + # Assign the variable "localOut" to the output location where you expect your program to put files + # e.g. localOut = os.path.join(LOCAL_OUTPUT, metadataID) + + print("Running", cmd) logger.info(cmd) - subp = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - monitorAndLog(subp,logger) + subp = subprocess.Popen( + cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) + monitorAndLog(subp, logger) - # Figure out a done condition - a number of files being created, a particular file being created, an exit code, etc. + # Figure out a done condition - a number of files being created, a particular file/folder being created, an exit code, etc. # Set its success to the boolean variable `done` - + # e.g. done = os.path.isfile(os.path.join(localOut, program.is.done)) + # Get the outputs and move them to S3 if done: time.sleep(30) - mvtries=0 - while mvtries <3: + mvtries = 0 + while mvtries < 3: try: - printandlog('Move attempt #'+str(mvtries+1),logger) - cmd = 'aws s3 mv ' + localOut + ' s3://' + AWS_BUCKET + '/' + remoteOut + ' --recursive' - subp = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out,err = subp.communicate() - out=out.decode() - err=err.decode() - printandlog('== OUT \n'+out, logger) - if err == '': - break - else: - printandlog('== ERR \n'+err,logger) - mvtries+=1 + printandlog("Move attempt #" + str(mvtries + 1), logger) + cmd = ( + "aws s3 mv " + + localOut + + " s3://" + + AWS_BUCKET + + "/" + + remoteOut + + " --recursive" + ) + subp = subprocess.Popen( + cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + out, err = subp.communicate() + out = out.decode() + err = err.decode() + printandlog("== OUT \n" + out, logger) + if err == "": + break + else: + printandlog("== ERR \n" + err, logger) + mvtries += 1 except: - printandlog('Move failed',logger) - printandlog('== ERR \n'+err,logger) + printandlog("Move failed", logger) + printandlog("== ERR \n" + err, logger) time.sleep(30) - mvtries+=1 + mvtries += 1 if mvtries < 3: - printandlog('SUCCESS',logger) + printandlog("SUCCESS", logger) logger.removeHandler(watchtowerlogger) - return 'SUCCESS' + return "SUCCESS" else: - printandlog('SYNC PROBLEM. Giving up on trying to sync '+metadataID,logger) + printandlog( + "SYNC PROBLEM. Giving up on trying to sync " + metadataID, logger + ) import shutil + shutil.rmtree(localOut, ignore_errors=True) logger.removeHandler(watchtowerlogger) - return 'PROBLEM' + return "PROBLEM" else: - printandlog('PROBLEM: Failed exit condition for '+metadataID,logger) + printandlog("PROBLEM: Failed exit condition for " + metadataID, logger) logger.removeHandler(watchtowerlogger) import shutil + shutil.rmtree(localOut, ignore_errors=True) - return 'PROBLEM' - + return "PROBLEM" + ################################# # MAIN WORKER LOOP ################################# + def main(): queue = JobQueue(QUEUE_URL) # Main loop. Keep reading messages while they are available in SQS @@ -197,23 +233,23 @@ def main(): msg, handle = queue.readMessage() if msg is not None: result = runSomething(msg) - if result == 'SUCCESS': - print('Batch completed successfully.') + if result == "SUCCESS": + print("Batch completed successfully.") queue.deleteMessage(handle) else: - print('Returning message to the queue.') + print("Returning message to the queue.") queue.returnMessage(handle) else: - print('No messages in the queue') + print("No messages in the queue") break + ################################# # MODULE ENTRY POINT ################################# -if __name__ == '__main__': +if __name__ == "__main__": logging.basicConfig(level=logging.INFO) - print('Worker started') + print("Worker started") main() - print('Worker finished') - + print("Worker finished") diff --git a/worker/instance-monitor.py b/worker/instance-monitor.py index dec511a..8ffca40 100644 --- a/worker/instance-monitor.py +++ b/worker/instance-monitor.py @@ -9,16 +9,18 @@ import time import logging + def monitor(): logger = logging.getLogger(__name__) while True: - cmdlist=['df -h', 'df -i -h','vmstat -a -SM 1 1', 'iostat'] + cmdlist = ["df -h", "df -i -h", "vmstat -a -SM 1 1", "iostat"] for cmd in cmdlist: - process=subprocess.Popen(cmd.split(),stdout=subprocess.PIPE) - out,err=process.communicate() + process = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE) + out, err = process.communicate() logger.info(out) time.sleep(30) - -if __name__=='__main__': + + +if __name__ == "__main__": logging.basicConfig(level=logging.INFO) monitor() diff --git a/worker/run-worker.sh b/worker/run-worker.sh index fb64ad8..6fbf701 100644 --- a/worker/run-worker.sh +++ b/worker/run-worker.sh @@ -17,15 +17,16 @@ aws ec2 create-tags --resources $VOL_0_ID --tags Key=Name,Value=${APP_NAME}Worke VOL_1_ID=$(aws ec2 describe-instance-attribute --instance-id $MY_INSTANCE_ID --attribute blockDeviceMapping --output text --query BlockDeviceMappings[1].Ebs.[VolumeId]) aws ec2 create-tags --resources $VOL_1_ID --tags Key=Name,Value=${APP_NAME}Worker -# 2. MOUNT S3 +# 2. MOUNT S3 +# Remove this if not mounting S3 bucket echo $AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY > /credentials.txt chmod 600 /credentials.txt mkdir -p /home/ubuntu/bucket mkdir -p /home/ubuntu/local_output -stdbuf -o0 s3fs $AWS_BUCKET /home/ubuntu/bucket -o passwd_file=/credentials.txt +stdbuf -o0 s3fs $AWS_BUCKET /home/ubuntu/bucket -o passwd_file=/credentials.txt # 3. SET UP ALARMS -aws cloudwatch put-metric-alarm --alarm-name ${APP_NAME}_${MY_INSTANCE_ID} --alarm-actions arn:aws:swf:${AWS_REGION}:${OWNER_ID}:action/actions/AWS_EC2.InstanceId.Terminate/1.0 --statistic Maximum --period 60 --threshold 1 --comparison-operator LessThanThreshold --metric-name CPUUtilization --namespace AWS/EC2 --evaluation-periods 15 --dimensions "Name=InstanceId,Value=${MY_INSTANCE_ID}" +aws cloudwatch put-metric-alarm --alarm-name ${APP_NAME}_${MY_INSTANCE_ID} --alarm-actions arn:aws:swf:${AWS_REGION}:${OWNER_ID}:action/actions/AWS_EC2.InstanceId.Terminate/1.0 --statistic Maximum --period 60 --threshold 1 --comparison-operator LessThanThreshold --metric-name CPUUtilization --namespace AWS/EC2 --evaluation-periods 15 --dimensions "Name=InstanceId,Value=${MY_INSTANCE_ID}" # 4. RUN VM STAT MONITOR @@ -37,4 +38,3 @@ for((k=0; k<$DOCKER_CORES; k++)); do sleep $SECONDS_TO_START done wait - From 742f75ad7aacf77957b493fcbfc33c6350cac67f Mon Sep 17 00:00:00 2001 From: ErinWeisbart Date: Wed, 13 Jul 2022 14:56:19 -0700 Subject: [PATCH 04/10] work on documentation --- documentation/DS-documentation/_toc.yml | 4 +- .../DS-documentation/customizing_DS.md | 67 ++++++++++++++++++- .../DS-documentation/implementing_DS.md | 19 ++++++ .../DS-documentation/step_2_submit_jobs.md | 3 +- .../troubleshooting_implementation.md | 25 +++++++ ...bleshooting.md => troubleshooting_runs.md} | 0 6 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 documentation/DS-documentation/implementing_DS.md create mode 100644 documentation/DS-documentation/troubleshooting_implementation.md rename documentation/DS-documentation/{troubleshooting.md => troubleshooting_runs.md} (100%) diff --git a/documentation/DS-documentation/_toc.yml b/documentation/DS-documentation/_toc.yml index 9d427ad..d3cf971 100644 --- a/documentation/DS-documentation/_toc.yml +++ b/documentation/DS-documentation/_toc.yml @@ -7,6 +7,8 @@ parts: - caption: chapters: - file: customizing_DS + - file: implementing_DS + - file: troubleshooting_implementation - caption: Running Distributed-Something chapters: - file: step_0_prep @@ -18,5 +20,5 @@ parts: - file: step_4_monitor - caption: chapters: - - file: troubleshooting + - file: troubleshooting_runs - file: versions diff --git a/documentation/DS-documentation/customizing_DS.md b/documentation/DS-documentation/customizing_DS.md index 7856cab..f3125b5 100644 --- a/documentation/DS-documentation/customizing_DS.md +++ b/documentation/DS-documentation/customizing_DS.md @@ -1,6 +1,67 @@ # Customizing Distributed-Something Distributed-Something is a template. -Examples of implementations can be found at Distributed-CellProfiler, Distributed-Fiji, and Distributed-BioFormats2Raw. -There are many points at which you will need to customize Distributed-Something for your own implementation. -These customization points are documented within each file and summarized below. +It is not fully functional software but is intended to serve as an editable source so that you can quickly and easily implement a distributed workflow for your own dockerized software. + +Examples of implementations can be found at [Distributed-CellProfiler](http://github.com/cellprofiler/distributed-cellprofiler), [Distributed-Fiji](http://github.com/cellprofiler/distributed-fiji), and [Distributed-OmeZarrMaker](http://github.com/cellprofiler/distributed-omezarrmaker). + +There are many points at which you will need to customize Distributed-Something for your own implementation; These customization points are summarized below. +Files that do not require any customization are not listed. + +## files/ + +### exampleFleet.json + +exampleFleet.json does not need to be changed depending on your implementation of Distributed-Something. +However, each AWS account running your implementation will need to update the Fleet file with configuration specific to their account as detailed in [Step 3: Start Cluster](step_3_start_cluster.md). + +### exampleJob.json + +exampleJob.json needs to be entirely customized for your implementation of Distributed-Something. +When you submit your jobs in [Step 2: Submit Jobs](step_2_submit_jobs.md), Distributed-Something adds a job to your SQS queue for each item in `groups`. +Each job contains the shared variables common to all jobs, listed in the exampleJob.json above the `groups` key. +These variables are passed to your worker as the `message` and should include any metadata that may possibly change between runs of your Distributed-Something implementation. + +Some common variables used in Job files include: +- input location +- output location +- output structure +- script/pipeline name +- flags to pass to your program + +## worker/ + +### Dockerfile + +The Dockerfile is used to create the Distributed-Something Docker. +You will need to edit the `FROM` to point to your own docker. + +No further edits to the Dockerfile should be necessary, though advanced users make additional customizations based on the docker they are `FROM`ing. +Additionally, you may remove the section `# Install S3FS` if your workflow doesn't require mounting an S3 bucket. +You will still be able to upload and download from an S3 bucket using AWS CLI without mounting it with S3FS. + +### generic-worker.py + +The majority of code customization for your implementation of Distributed-Something happens in the worker file. +The `generic-worker.py` code is thoroughly documented with customization details. + +### Makefile + +Update `user` and `project` to match you and your Distributed-Something implementation, respectively. + +### run-worker.sh + +You do not need to make any modifications to run-worker.sh. +You might want to remove `2. MOUNT S3` if your workflow doesn't require mounting an S3 bucket. + +## other files + +### config.py + +`DOCKERHUB_TAG` needs to match the `user` and `project` set in `Makefile`. +We recommend adjusting `EC2 AND ECS INFORMATION` and `DOCKER INSTANCE RUNNING ENVIRONMENT` variables to reasonable defaults for your Distributed-Something implementation. + +If there are any variables you would like to pass to your program as part of configuration, you can add them at the bottom and they will be passed as system variables to the Docker. +Note that any additional variables added to `config.py` need to also be added to `CONSTANT PATHS IN THE CONTAINER` in `generic-worker.py`. + +`AWS GENERAL SETTINGS` are specific to your account. All other sections are variable specific to each batch/run of your Distributed-Something implementation and will need to be adjusted at each run time. More configuration information is available in [Step 1: Configuration](step_1_configuration.md) diff --git a/documentation/DS-documentation/implementing_DS.md b/documentation/DS-documentation/implementing_DS.md new file mode 100644 index 0000000..c21c171 --- /dev/null +++ b/documentation/DS-documentation/implementing_DS.md @@ -0,0 +1,19 @@ +# Implementing your Distributed-Something + +## Make your software docker + +## Make your Distributed-Something docker + +How to make a docker: +do this stuff +`make` +Note that you can set :latest in makefile and in config.py + +## Test requirements for config.py + +To test necessary parameters: +Create Ec2 instance. +Use AMI with S3FS already installed +setup cloudwatch agent for more comprehensive usage stats (https://docs.google.com/document/d/1Gi8eHRmvTrQUdBO2aUjby_bzW5iUV68xcSIm_NPUhPo/edit) +install docker with `sudo apt install docker.io` +`sudo docker run -it --rm --entrypoint /bin/sh -v ~/bucket:/bucket DOCKER` diff --git a/documentation/DS-documentation/step_2_submit_jobs.md b/documentation/DS-documentation/step_2_submit_jobs.md index c33c64d..ef2a64a 100644 --- a/documentation/DS-documentation/step_2_submit_jobs.md +++ b/documentation/DS-documentation/step_2_submit_jobs.md @@ -3,12 +3,11 @@ Distributed-Something works by breaking your workflow into a series of smaller jobs based on the metadata and groupings you've specified in your job file. The choice of how to group your jobs is largely dependent on the details of your workflow. Once you've decided on a grouping, you're ready to start configuring your job file. -Once your job file is configured, simply use `python run.py submitJob files/{YourJobFile}.json` to send all the jobs to the SQS queue [[specified in your config file|Step-1:--Configuration]]. +Once your job file is configured, simply use `python run.py submitJob files/{YourJobFile}.json` to send all the jobs to the SQS queue [specified in your config file](step_1_configuration.md). ## Configuring your job file All keys (outside of your groups) are shared between all jobs. -Common keys include **input_location**, **output_location**, **script_to_use**. * **groups:** The list of all the groups you'd like to process. Keys within each job can either be used to define the job (e.g. Metadata, file location) or can be used to pass job-specific variables. diff --git a/documentation/DS-documentation/troubleshooting_implementation.md b/documentation/DS-documentation/troubleshooting_implementation.md new file mode 100644 index 0000000..90e29ff --- /dev/null +++ b/documentation/DS-documentation/troubleshooting_implementation.md @@ -0,0 +1,25 @@ +# Troubleshooting Your Distributed-Something Implementation + +## Check the Logs + +## AWS Credential Handling +run.py uses default credentials from computer to run following commands. +run.py doesn't pass flags to the various AWS commands so you need to have AWS CLI set up with default account having these permissions. +- setup: needs SQS permissions to create queue and and ECS permissions to create cluster and task definition. +Sends environment variables to task definition that include either role or access keys. +- submitJob: needs SQS permissions to add jobs to queue. +- startCluster: needs ECS permissions to create ECS config, S3 permissions to upload ECS config, EC2 permissions to launch a spot fleet, Cloudwatch permissions to create logs. + +Once an EC2 instance is launched, ECS automatically puts a docker on that instance. +Fleet file determines IamFleetRole and the IamInstanceProfile. One or both of those contains permission for ECS to put the docker, and then anything that is needed in the Dockerfile (may be nothing. Could be mounting s3fs) +Dockerfile is executed. No AWS credentials are used in this step. + +Dockerfile enters run-worker.sh. +run-worker.sh configures AWS CLI with environment variables. +run-worker.sh configures the EC2 instance and then launches workers with generic-worker.py. +generic-worker.py runs + + +If you're having a hard time with credentials, you can pass `curl 169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` in run-worker.sh to show the credentials that the docker is using. or `aws configure list` + +If you pass a key/secret key and a role it will default to key/secret key to task. diff --git a/documentation/DS-documentation/troubleshooting.md b/documentation/DS-documentation/troubleshooting_runs.md similarity index 100% rename from documentation/DS-documentation/troubleshooting.md rename to documentation/DS-documentation/troubleshooting_runs.md From afcf35b9167feaa7d6779d9cdd24e1d6d8e68058 Mon Sep 17 00:00:00 2001 From: ErinWeisbart Date: Thu, 14 Jul 2022 13:16:20 -0700 Subject: [PATCH 05/10] more documentation updates --- LICENSE | 4 +- README.md | 56 +++++++++---------- documentation/.github/workflows/deploy.yml | 3 +- .../DS-documentation/SQS_QUEUE_information.md | 6 +- documentation/DS-documentation/_toc.yml | 4 +- .../DS-documentation/customizing_DS.md | 3 +- .../DS-documentation/implementing_DS.md | 56 +++++++++++++++---- documentation/DS-documentation/overview.md | 6 +- .../troubleshooting_implementation.md | 37 +++++++----- .../DS-documentation/troubleshooting_runs.md | 4 +- documentation/README.md | 25 +++++---- 11 files changed, 119 insertions(+), 85 deletions(-) diff --git a/LICENSE b/LICENSE index 6e000a0..1d6c29f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -Distributed-CellProfiler is distributed under the following BSD-style license: +Distributed-Something is distributed under the following BSD-style license: -Copyright © 2020 Broad Institute, Inc. All rights reserved. +Copyright © 2022 Broad Institute, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are diff --git a/README.md b/README.md index 3d7df42..246dcc1 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,62 @@ # Distributed-Something -Run encapsulated docker containers that do... something in the Amazon Web Services infrastructure. -It could be [CellProfiler](https://github.com/CellProfiler/Distributed-CellProfiler) or [Fiji](https://github.com/CellProfiler/Distributed-Fiji) or really whatever you want. +Run encapsulated docker containers that do... something in the Amazon Web Services (AWS) infrastructure. +We are interested in scientific image analysis so we have used it for [CellProfiler](https://github.com/CellProfiler/Distributed-CellProfiler), [Fiji](https://github.com/CellProfiler/Distributed-Fiji), and [BioFormats2Raw](https://github.com/CellProfiler/Distributed-OmeZarrMaker). +You can use it for whatever you want! [Here's how you adapt this to whatever you need to Distribute](https://github.com/CellProfiler/Distributed-Something/wiki) -This code is an example of how to use AWS distributed infrastructure for running anything dockerized. -The configuration of the AWS resources is done using boto3 and the awscli. The worker is written in Python -and is encapsulated in a docker container. There are four AWS components that are minimally -needed to run distributed jobs: +This code is an example of how to use AWS distributed infrastructure for running anything Dockerized. +The configuration of the AWS resources is done using boto3 and the AWS CLI. +The worker is written in Python and is encapsulated in a Docker container. There are four AWS components that are minimally needed to run distributed jobs: 1. An SQS queue 2. An ECS cluster 3. An S3 bucket 4. A spot fleet of EC2 instances -All of them can be managed through the AWS Management Console. However, this code helps to get -started quickly and run a job autonomously if all the configuration is correct. The code includes -prepares the infrastructure to run a distributed job. When the job is completed, the code is also -able to stop resources and clean up components. It also adds logging and alarms via CloudWatch, -helping the user troubleshoot runs and destroy stuck machines. +All of them can be managed individually through the AWS Management Console. +However, this code helps to get started quickly and run a job autonomously if all the configuration is correct. +The code prepares the infrastructure to run a distributed job. +When the job is completed, the code is also able to stop resources and clean up components. +It also adds logging and alarms via CloudWatch, helping the user troubleshoot runs and destroy stuck machines. ## Running the code ### Step 1 -Edit the config.py file with all the relevant information for your job. Then, start creating -the basic AWS resources by running the following script: +Edit the `config.py` file with all the relevant information for your job. Then, start creating the basic AWS resources by running the following script: $ python run.py setup -This script intializes the resources in AWS. Notice that the docker registry is built separately, -and you can modify the worker code to build your own. Anytime you modify the worker code, you need -to update the docker registry using the Makefile script inside the worker directory. +This script initializes the resources in AWS. +Notice that the docker registry is built separately and you can modify the worker code to build your own. +Any time you modify the worker code, you need to update the docker registry using the Makefile script inside the worker directory. ### Step 2 -After the first script runs successfully, the job can now be submitted to with the -following command: +After the first script runs successfully, the job can now be submitted to with the following command: $ python run.py submitJob files/exampleJob.json -Running the script uploads the tasks that are configured in the json file. You have to -customize the exampleJob.json file with information that make sense for your project. +Running the script uploads the tasks that are configured in the json file. You have to customize the `exampleJob.json` file with information that makes sense for your project. You'll want to figure out which information is generic and which is the information that makes each job unique. ### Step 3 -After submitting the job to the queue, we can add computing power to process all tasks in AWS. This -code starts a fleet of spot EC2 instances which will run the worker code. The worker code is encapsulated -in docker containers, and the code uses ECS services to inject them in EC2. All this is automated -with the following command: +After submitting the job to the queue, we can add computing power to process all tasks in AWS. +This code starts a fleet of spot EC2 instances which will run the worker code. +The worker code is encapsulated in Docker containers, and the code uses ECS services to inject them in EC2. +All this is automated with the following command: $ python run.py startCluster files/exampleFleet.json -After the cluster is ready, the code informs you that everything is setup, and saves the spot fleet identifier -in a file for further reference. +After the cluster is ready, the code informs you that everything is setup, and saves the spot fleet identifier in a file for further reference. ### Step 4 When the cluster is up and running, you can monitor progress using the following command: $ python run.py monitor files/APP_NAMESpotFleetRequestId.json -The file APP_NAMESpotFleetRequestId.json is created after the cluster is setup in step 3. It is - -important to keep this monitor running if you want to automatically shutdown computing resources -when there are no more tasks in the queue (recommended). +The file APP_NAMESpotFleetRequestId.json is created after the cluster is setup in step 3. +It is important to keep this monitor running if you want to automatically shutdown computing resources when there are no more tasks in the queue (recommended). See the wiki for more information about each step of the process. -![DistributedSomething](https://user-images.githubusercontent.com/6721515/148241641-7e447d94-dc25-4214-afb1-132e3dc06987.png) +![Distributed-Something](https://user-images.githubusercontent.com/6721515/148241641-7e447d94-dc25-4214-afb1-132e3dc06987.png) diff --git a/documentation/.github/workflows/deploy.yml b/documentation/.github/workflows/deploy.yml index 0e64823..e23d56f 100644 --- a/documentation/.github/workflows/deploy.yml +++ b/documentation/.github/workflows/deploy.yml @@ -1,4 +1,4 @@ -name: deploy +name: deploy-documentation # Only run this when the master branch changes on: @@ -37,4 +37,3 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./DS-documentation/_build/html - cname: distributed-something.github.io diff --git a/documentation/DS-documentation/SQS_QUEUE_information.md b/documentation/DS-documentation/SQS_QUEUE_information.md index 33e9522..74e2a50 100644 --- a/documentation/DS-documentation/SQS_QUEUE_information.md +++ b/documentation/DS-documentation/SQS_QUEUE_information.md @@ -10,9 +10,9 @@ This is in-depth information about the configurable components in SQS QUEUE INFO **SQS_DEAD_LETTER_QUEUE** is the name of the queue where all the jobs that failed to run are sent. If everything goes perfectly, this will always remain empty. If jobs that are in the queue fail multiple times (our default is 10) they are moved to the dead-letter queue, which is not used to initiate jobs. The dead-letter queue therefore functions effectively as a log so you can see if any of your jobs failed. It is different from your other queue as machines do not try and pull jobs from it. Protip: Each member of our team maintains their own dead-letter queue so we don’t have to worry about finding messages if multiple people are running jobs at the same time. We use names like DeadMessages_Erin. -If all of your jobs end up in your dead-letter queue there are many different places you could have a problem. Hopefully, you’ll keep an eye on the logs in your Cloudwatch (the part of AWS used for monitoring what all your other AWS services are doing) after starting a run and catch the issue before all of your jobs fail multiple times. +If all of your jobs end up in your dead-letter queue there are many different places you could have a problem. Hopefully, you’ll keep an eye on the logs in your CloudWatch (the part of AWS used for monitoring what all your other AWS services are doing) after starting a run and catch the issue before all of your jobs fail multiple times. -If a single job ends up in your dead-letter queue while the rest of your jobs complete successfully, it is likely that that an image is corrupted (a corrupted image is one that has failed to save properly or has been damaged so that it will not open). This is true whether your pipeline processes a single image at a time (such as in analysis runs where you’re interested in cellular measurements on a per-image basis) or whether your pipeline processes many images at a time (such as when making an illumination correction image on a per-plate basis). This is the major reason why we have the dead-letter queue: you certainly don’t want to pay for your cluster to indefinitely attempt to process a corrupted image. Keeping an eye on your Cloudwatch logs wouldn’t necessarily help you catch this kind of error because you could have tens or hundreds of successful jobs run before an instance pulls the job for the corrupted image, or the corrupted image could be thousands of images into an illumination correction run, etc. +If a single job ends up in your dead-letter queue while the rest of your jobs complete successfully, it is likely that that an image is corrupted (a corrupted image is one that has failed to save properly or has been damaged so that it will not open). This is true whether your pipeline processes a single image at a time (such as in analysis runs where you’re interested in cellular measurements on a per-image basis) or whether your pipeline processes many images at a time (such as when making an illumination correction image on a per-plate basis). This is the major reason why we have the dead-letter queue: you certainly don’t want to pay for your cluster to indefinitely attempt to process a corrupted image. Keeping an eye on your CloudWatch logs wouldn’t necessarily help you catch this kind of error because you could have tens or hundreds of successful jobs run before an instance pulls the job for the corrupted image, or the corrupted image could be thousands of images into an illumination correction run, etc. ## SQS_MESSAGE_VISIBILITY @@ -30,7 +30,7 @@ If the SQS_MESSAGE_VISIBILITY is too short then a job will become unhidden even If the SQS_MESSAGE_VISIBILITY is too long then you can end up wasting time and money waiting for the job to become available again after a crash even when the rest of your analysis is done. If anything causes a job to stop mid-run (e.g. CellProfiler crashes, the instance crashes, or the instance is removed by AWS because you are outbid), that job stays hidden until the set time. If a Docker instance goes to the queue and doesn’t find any visible jobs, then it does not try to run any more jobs in that copy of CellProfiler, limiting the effective computing power of that Docker. Therefore, some or all of your instances may hang around doing nothing (but costing money) until the job is visible again. When in doubt, it is better to have your SQS_MESSAGE_VISIBILITY set too long than too short because, while crashes can happen, it is rare that AWS takes small machines from your fleet, though we do notice it happening with larger machines. -There is not an easy way to see if you have selected the appropriate amount of time for your SQS_MESSAGE_VISIBILITY on your first run through a brand new pipeline. To confirm that multiple Dockers didn’t run the same job, after the jobs are complete, you need to manually go through each log in Cloudwatch and figure out how many times you got the word “SUCCESS” in each log. (This may be reasonable to do on an illumination correction run where you have a single job per plate, but it’s not so reasonable if running an analysis pipeline on thousands of individual images). To confirm that multiple Dockers are never processing the same job, you can keep an eye on your queue and make sure that you never have more jobs “In Flight” than the number of copies of CellProfiler that you have running; likewise, if your timeout time is too short, it may seem like too few jobs are “In Flight” even though the CPU usage on all your machines is high. +There is not an easy way to see if you have selected the appropriate amount of time for your SQS_MESSAGE_VISIBILITY on your first run through a brand new pipeline. To confirm that multiple Dockers didn’t run the same job, after the jobs are complete, you need to manually go through each log in CloudWatch and figure out how many times you got the word “SUCCESS” in each log. (This may be reasonable to do on an illumination correction run where you have a single job per plate, but it’s not so reasonable if running an analysis pipeline on thousands of individual images). To confirm that multiple Dockers are never processing the same job, you can keep an eye on your queue and make sure that you never have more jobs “In Flight” than the number of copies of CellProfiler that you have running; likewise, if your timeout time is too short, it may seem like too few jobs are “In Flight” even though the CPU usage on all your machines is high. Once you have run a pipeline once, you can check the execution time (either by noticing how long after you started your jobs that your first jobs begin to finish, or by checking the logs of individual jobs and noting the start and end time), you will then have an accurate idea of roughly how long that pipeline needs to execute, and can set your message visibility accordingly. You can even do this on the fly while jobs are currently processing; the updated visibility time won’t affect the jobs already out for processing (ie if the time was set to 3 hours and you change it to 1 hour, the jobs already processing will remain hidden for 3 hours or until finished), but any job that begins processing AFTER the change will use the new visibility timeout setting. diff --git a/documentation/DS-documentation/_toc.yml b/documentation/DS-documentation/_toc.yml index d3cf971..5035b2f 100644 --- a/documentation/DS-documentation/_toc.yml +++ b/documentation/DS-documentation/_toc.yml @@ -4,12 +4,12 @@ format: jb-book root: overview parts: -- caption: +- caption: Creating DS chapters: - file: customizing_DS - file: implementing_DS - file: troubleshooting_implementation -- caption: Running Distributed-Something +- caption: Running DS chapters: - file: step_0_prep - file: step_1_configuration diff --git a/documentation/DS-documentation/customizing_DS.md b/documentation/DS-documentation/customizing_DS.md index f3125b5..24ed4e6 100644 --- a/documentation/DS-documentation/customizing_DS.md +++ b/documentation/DS-documentation/customizing_DS.md @@ -1,4 +1,4 @@ -# Customizing Distributed-Something +# Customizing DS Distributed-Something is a template. It is not fully functional software but is intended to serve as an editable source so that you can quickly and easily implement a distributed workflow for your own dockerized software. @@ -60,6 +60,7 @@ You might want to remove `2. MOUNT S3` if your workflow doesn't require mounting `DOCKERHUB_TAG` needs to match the `user` and `project` set in `Makefile`. We recommend adjusting `EC2 AND ECS INFORMATION` and `DOCKER INSTANCE RUNNING ENVIRONMENT` variables to reasonable defaults for your Distributed-Something implementation. +Suggestions for determining optimal parameters can be found in [Implementing Distributed-Something](implementing_DS.md). If there are any variables you would like to pass to your program as part of configuration, you can add them at the bottom and they will be passed as system variables to the Docker. Note that any additional variables added to `config.py` need to also be added to `CONSTANT PATHS IN THE CONTAINER` in `generic-worker.py`. diff --git a/documentation/DS-documentation/implementing_DS.md b/documentation/DS-documentation/implementing_DS.md index c21c171..fca67d1 100644 --- a/documentation/DS-documentation/implementing_DS.md +++ b/documentation/DS-documentation/implementing_DS.md @@ -1,19 +1,51 @@ -# Implementing your Distributed-Something +# Implementing DS -## Make your software docker +## Make your software Docker -## Make your Distributed-Something docker +Your software will ned to be containerized in its own Docker. +This Docker image is what you will `FROM` in `Dockerfile` when you create your Distributed-Something Docker image. +Detailed instructions are out of the scope of this documentation, though we refer you to [Docker's documentation](https://docs.docker.com/get-started/). +For examples that we use in our Distributed-Something suite, you can refer to the code used to make the [CellProfiler Docker](https://github.com/CellProfiler/distribution/tree/master/docker), [BioFormats2Raw Docker](https://github.com/ome/bioformats2raw-docker), and [Fiji Docker](https://github.com/fiji/dockerfiles). -How to make a docker: -do this stuff -`make` -Note that you can set :latest in makefile and in config.py +## Make your Distributed-Something Docker + +Once you have made all the alterations to the Distributed-Something code detailed in [Customizing Distributed-Something](customizing_DS.md), you need to make your Distributed-Something Docker image. + +You will need a [DockerHub account](https://hub.docker.com). + +
+# Navigate into the Distributed-Something/worker folder
+cd worker
+# Run the make command
+make
+
+ +While it is generally a good principle to iterate numerical tags, note that you can set the tag to `latest` in both `Makefile` and in `config.py` to simplify troubleshooting (as you don't have to remember to change the tag in either location while potentially testing multiple Docker builds). ## Test requirements for config.py +Once you have created a functional Docker image of your software, it is useful to know exact memory requirements so you can request appropriately sized machines in `config.py`. +We recommend using CloudWatch Agent on an AWS instance. +(Standard CloudWatch metrics do not report granular memory usage.) + To test necessary parameters: -Create Ec2 instance. -Use AMI with S3FS already installed -setup cloudwatch agent for more comprehensive usage stats (https://docs.google.com/document/d/1Gi8eHRmvTrQUdBO2aUjby_bzW5iUV68xcSIm_NPUhPo/edit) -install docker with `sudo apt install docker.io` -`sudo docker run -it --rm --entrypoint /bin/sh -v ~/bucket:/bucket DOCKER` +- Create a EC2 instance using an AMI with S3FS already installed. +- Add an IAM role. This machine must have an instance role with sufficient permissions attached in order to transmit metrics. +- Connect to your EC2 instance. +- Install CloudWatch Agent following [AWS documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/download-cloudwatch-agent-commandline.html). + +
+# Start CloudWatch Agent
+sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -s -c file:/opt/aws/amazon-cloudwatch-agent/bin/config.json
+# Install Docker
+sudo apt install docker.io
+# Run
+sudo docker run -it --rm --entrypoint /bin/sh -v ~/bucket:/bucket DOCKER
+# Stop CloudWatch Agent
+sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -m ec2 -a stop
+
+ +View your collected memory metrics in CloudWatch: +- CloudWatch => Metrics => All Metrics +- Custom Namespaces => CWAgent => ImageId, InstanceId, InstanceType => YOUR_INSTANCE mem_used_percent +- Graphed metrics => PERIOD = 1 Minute (or whatever you have set in your CloudWatch config) diff --git a/documentation/DS-documentation/overview.md b/documentation/DS-documentation/overview.md index 282b558..880ef44 100644 --- a/documentation/DS-documentation/overview.md +++ b/documentation/DS-documentation/overview.md @@ -1,8 +1,8 @@ # What is Distributed-Something? -Distributed-Something is a series of scripts designed to help you run a Dockerized version of your software on [Amazon Web Services](https://aws.amazon.com/) using AWS's file storage and computing systems. +Distributed-Something is a series of scripts designed to help you run a Dockerized version of your software on [Amazon Web Services](https://aws.amazon.com/) (AWS) using AWS's file storage and computing systems. * Data is stored in S3 buckets. -* Software is run on "SpotFleets" of computers (or "instances") in the cloud. +* Software is run on "Spot Fleets" of computers (or instances) in the cloud. You will need to customize Distributed-Something for your particular use case. See [Customizing Distributed-Something](customizing_DS.md) for customization details. @@ -53,7 +53,7 @@ ECS will keep placing Dockers onto an instance until it is full, so if you accid This is also why you may want multiple **ECS_CLUSTER**s so that ECS doesn't blindly place Dockers you intended for one job onto an instance you intended for another job. * When a Docker container gets placed it gives the instance it's on its own name. * Once an instance has a name, the Docker gives it an alarm that tells it to reboot if it is sitting idle for 15 minutes. -* The Docker hooks the instance up to the _perinstance logs in Cloudwatch. +* The Docker hooks the instance up to the _perinstance logs in CloudWatch. * The instances look in SQS for a job. Any time they don't have a job they go back to SQS. If SQS tells them there are no visible jobs then they shut themselves down. diff --git a/documentation/DS-documentation/troubleshooting_implementation.md b/documentation/DS-documentation/troubleshooting_implementation.md index 90e29ff..113b44a 100644 --- a/documentation/DS-documentation/troubleshooting_implementation.md +++ b/documentation/DS-documentation/troubleshooting_implementation.md @@ -1,25 +1,32 @@ -# Troubleshooting Your Distributed-Something Implementation +# Troubleshooting Your DS Implementation ## Check the Logs +Logs are automatically created in CloudWatch as a part of Distributed-Something. +If you need to troubleshoot your Distributed-Something implementation, it is likely that your error will be logged in CloudWatch. +`LOG_GROUP_NAME_perInstance` logs contain a log for every EC2 instance that is launched as a part of your Spot Fleet request and should be the first place that you look. +If your instances are successfully able to pull messages from the SQS queue, they will create a log for each job which can be easier to parse than the full `_perInstance` logs. ## AWS Credential Handling -run.py uses default credentials from computer to run following commands. +Improper credential handling can be a blocking point in accessing many AWS services as all permissions must be explicitly granted in AWS and best practices are to set least-privilege permissions. +Distributed-Something is configured to simplify access management, but if you need to make changes to any of the recommended access management, be sure that you have carefully considered permissions. + +Some of the required permissions are as follows: +run.py uses default credentials from your computer to run the following commands. run.py doesn't pass flags to the various AWS commands so you need to have AWS CLI set up with default account having these permissions. -- setup: needs SQS permissions to create queue and and ECS permissions to create cluster and task definition. -Sends environment variables to task definition that include either role or access keys. -- submitJob: needs SQS permissions to add jobs to queue. -- startCluster: needs ECS permissions to create ECS config, S3 permissions to upload ECS config, EC2 permissions to launch a spot fleet, Cloudwatch permissions to create logs. +- `setup`: needs SQS permissions to create the queue and and ECS permissions to create the cluster and task definition. +It sends environment variables to the task definition that include either role or access keys. +If you pass a key/secret key and a role it will default to using the key/secret key for the task. +- `submitJob`: needs SQS permissions to add jobs to queue. +- `startCluster`: needs ECS permissions to create ECS config, S3 permissions to upload ECS config, EC2 permissions to launch a spot fleet, and CloudWatch permissions to create logs. Once an EC2 instance is launched, ECS automatically puts a docker on that instance. -Fleet file determines IamFleetRole and the IamInstanceProfile. One or both of those contains permission for ECS to put the docker, and then anything that is needed in the Dockerfile (may be nothing. Could be mounting s3fs) -Dockerfile is executed. No AWS credentials are used in this step. - -Dockerfile enters run-worker.sh. -run-worker.sh configures AWS CLI with environment variables. -run-worker.sh configures the EC2 instance and then launches workers with generic-worker.py. -generic-worker.py runs +The fleet file determines IamFleetRole and the IamInstanceProfile. One or both of those contains permission for ECS to put the docker, and then anything that is needed in the Dockerfile (may be nothing. Could be mounting s3fs) +No AWS credentials are used to execute `Dockerfile`. -If you're having a hard time with credentials, you can pass `curl 169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` in run-worker.sh to show the credentials that the docker is using. or `aws configure list` +`Dockerfile` enters `run-worker.sh`. +`run-worker.sh` configures AWS CLI with environment variables. +`run-worker.sh` configures the EC2 instance and then launches workers with `generic-worker.py`. +`generic-worker.py` interacts with the SQS queue, CloudWatch, S3, and whatever else is required by your software. -If you pass a key/secret key and a role it will default to key/secret key to task. +If you're having a hard time with determining what credentials have been passed to your Docker, you can add `curl 169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` or `aws configure list` in `run-worker.sh` to have it print the credentials that the docker is using. diff --git a/documentation/DS-documentation/troubleshooting_runs.md b/documentation/DS-documentation/troubleshooting_runs.md index 4cee473..ad4a89d 100644 --- a/documentation/DS-documentation/troubleshooting_runs.md +++ b/documentation/DS-documentation/troubleshooting_runs.md @@ -4,8 +4,8 @@ We recommend you create a troubleshooting table that describes common failure mo Shown below are errors common to most implementations of DS. Distributed-CellProfiler documentation has a [thorough example](https://github.com/CellProfiler/Distributed-CellProfiler/wiki/Troubleshooting). -| SQS | Cloudwatch | S3 | EC2/ECS | Problem | Solution | +| SQS | CloudWatch | S3 | EC2/ECS | Problem | Solution | |---|---|---|---|---|---| | | Within a single log, your run command is logging multiple times. | Expected output seen. | | A single job is being processed multiple times. | SQS_MESSAGE_VISIBILITY is set too short. See SQS-QUEUE-INFORMATION for more information. | -| | Your specified output structure does not match the Metadata passed. | Expected output is seen. | | This is not necessarily an error. If the input grouping is different than the output grouping (e.g. jobs are run by Plate-Well-Site but are all output to a single Plate folder) then this will print in the Cloudwatch log that matches the input structure but actual job progress will print in the Cloudwatch log that matches the output structure. | | +| | Your specified output structure does not match the Metadata passed. | Expected output is seen. | | This is not necessarily an error. If the input grouping is different than the output grouping (e.g. jobs are run by Plate-Well-Site but are all output to a single Plate folder) then this will print in the CloudWatch log that matches the input structure but actual job progress will print in the CloudWatch log that matches the output structure. | | | | | | Machines made in EC2 and dockers are made in ECS but the dockers are not placed on the machines. | There is a mismatch in your DS config file. | Confirm that the MEMORY matches the MACHINE_TYPE set in your config. | diff --git a/documentation/README.md b/documentation/README.md index 0e89c69..3b7e0fe 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -1,12 +1,13 @@ -Read more at -Make for your use case -Make sure you update documentation to fit your particular Distributed-Something implementation. - -Will auto generate for you at URL every time you push to main. -Can manually generate with - -Points at which documentation will need to be updated for your particular implementation: -- config.yml - - author - - repository:url: - - html:baseurl: +This folder contains the files to automatically generate documentation for your Distributed-Something distribution using [Jupyter Book](https://jupyterbook.org/en/stable/intro.html). +This documentation is linked from the main repository README and serves in place of a wiki. + +Instructions for customizing your Distributed-Something distribution are in the `CREATING DS` section and can be deleted from your repository's documentation you have completed creating your Distributed-Something distribution. + +Documentation for running Distributed-Something are in the `RUNNING DS` section and should be updated to fit your particular Distributed-Something implementation. + +This documentation will automatically re-build any time you push to your repository's `main` branch. +By default, only pages that have undergone edits will rebuild. + +To enable this auto-build, you need to set up a GitHub Action in your repository. +Read more about [GitHub Actions](https://help.github.com/en/actions) and about [hosting Jupyter Books with GitHub Actions](https://jupyterbook.org/en/stable/publish/gh-pages.html?highlight=github#automatically-host-your-book-with-github-actions). +It will auto-build the documentation to a `gh-pages` branch which will be visible at `githubusername.github.io/yourbookname`. From b6e6c8be18f4872f3172802af41219f4dbc6afb8 Mon Sep 17 00:00:00 2001 From: ErinWeisbart Date: Sat, 23 Jul 2022 11:25:39 -0700 Subject: [PATCH 06/10] minor documentation cleanup --- .../DS-documentation/customizing_DS.md | 2 +- documentation/DS-documentation/overview.md | 21 ++++++++----------- documentation/DS-documentation/step_0_prep.md | 5 +++-- .../DS-documentation/step_1_configuration.md | 8 +++---- .../DS-documentation/step_3_start_cluster.md | 2 +- .../DS-documentation/step_4_monitor.md | 8 +++---- 6 files changed, 21 insertions(+), 25 deletions(-) diff --git a/documentation/DS-documentation/customizing_DS.md b/documentation/DS-documentation/customizing_DS.md index 24ed4e6..803309b 100644 --- a/documentation/DS-documentation/customizing_DS.md +++ b/documentation/DS-documentation/customizing_DS.md @@ -1,7 +1,7 @@ # Customizing DS Distributed-Something is a template. -It is not fully functional software but is intended to serve as an editable source so that you can quickly and easily implement a distributed workflow for your own dockerized software. +It is not fully functional software but is intended to serve as an editable source so that you can quickly and easily implement a distributed workflow for your own Dockerized software. Examples of implementations can be found at [Distributed-CellProfiler](http://github.com/cellprofiler/distributed-cellprofiler), [Distributed-Fiji](http://github.com/cellprofiler/distributed-fiji), and [Distributed-OmeZarrMaker](http://github.com/cellprofiler/distributed-omezarrmaker). diff --git a/documentation/DS-documentation/overview.md b/documentation/DS-documentation/overview.md index 880ef44..dbbfd3e 100644 --- a/documentation/DS-documentation/overview.md +++ b/documentation/DS-documentation/overview.md @@ -23,12 +23,12 @@ You will also need a Dockerized version of your software. ## What happens in AWS when I run Distributed-Something? -The steps for actually running the Distributed-Something code are outlined in the repository [README](https://github.com/CellProfiler/Distributed-Something/blob/master/README.md), and details of the parameters you set in each step are on their respective Documentation pages ([Step 1: Config](step_1_configuration.md), [Step 2: Jobs](step_2_submit_jobs.md), [Step 3: Fleet](step_3_start_cluster.md), and optional [Step 4: Monitor](step_4_monitor.md)). +The steps for actually running the Distributed-Something code are outlined in the repository [README](https://github.com/DistributedScience/Distributed-Something/blob/master/README.md), and details of the parameters you set in each step are on their respective Documentation pages ([Step 1: Config](step_1_configuration.md), [Step 2: Jobs](step_2_submit_jobs.md), [Step 3: Fleet](step_3_start_cluster.md), and optional [Step 4: Monitor](step_4_monitor.md)). We'll give an overview of what happens in AWS at each step here and explain what AWS does automatically once you have it set up. **Step 1**: In the Config file you set quite a number of specifics that are used by EC2, ECS, SQS, and in making Dockers. -When you run `$ python run.py setup` to execute the Config, it does three major things: +When you run `$ python3 run.py setup` to execute the Config, it does three major things: * Creates task definitions. These are found in ECS. They define the configuration of the Dockers and include the settings you gave for **CHECK_IF_DONE_BOOL**, **DOCKER_CORES**, **EXPECTED_NUMBER_FILES**, and **MEMORY**. @@ -42,13 +42,12 @@ When you submit the Job file it adds that list of tasks to the queue in SQS (whi Submit jobs with `$ python3 run.py submitJob`. **Step 3**: -In the Fleet file you set the number and size of the EC2 instances you want. -Start the fleet with `$ python3 run.py startCluster`. +In the Config file you set the number and size of the EC2 instances you want. +This information, along with account-specific configuration in the Fleet file is used to start the fleet with `$ python3 run.py startCluster`. **After these steps are complete, a number of things happen automatically**: * ECS puts Docker containers onto EC2 instances. -Ideally, you have set the same parameters in your Config file and your Fleet file. -If there is a mismatch and the Docker is larger than the instance it will not be placed. +If there is a mismatch within your Config file and the Docker is larger than the instance it will not be placed. ECS will keep placing Dockers onto an instance until it is full, so if you accidentally create instances that are too large you may end up with more Dockers placed on it than intended. This is also why you may want multiple **ECS_CLUSTER**s so that ECS doesn't blindly place Dockers you intended for one job onto an instance you intended for another job. * When a Docker container gets placed it gives the instance it's on its own name. @@ -63,14 +62,12 @@ If SQS tells them there are no visible jobs then they shut themselves down. ![Example Instance Configuration](images/sample_DCP_config_1.png) -This is an example of one possible instance configuration using [Distributed-CellProfiler](http://github.com/cellprofiler/distributed-cellprofiler) as an example. This is one m4.16xlarge EC2 instance (64 CPUs, 250GB of RAM) with a 165 EBS volume mounted on it. A spot fleet could contain many such instances. - +This is an example of one possible instance configuration using [Distributed-CellProfiler](http://github.com/cellprofiler/distributed-cellprofiler) as an example. +This is one m4.16xlarge EC2 instance (64 CPUs, 250GB of RAM) with a 165 EBS volume mounted on it. A spot fleet could contain many such instances. It has 16 tasks (individual Docker containers). - Each Docker container uses 10GB of hard disk space and is assigned 4 CPUs and 15 GB of RAM (which it does not share with other Docker containers). - -Each container shares its individual resources among 4 copies of CellProfiler. Each copy of CellProfiler runs a pipeline on one "job", which can be anything from a single image to an entire 384 well plate or timelapse movie. - +Each container shares its individual resources among 4 copies of CellProfiler. +Each copy of CellProfiler runs a pipeline on one "job", which can be anything from a single image to an entire 384 well plate or timelapse movie. You can optionally stagger the start time of these 4 copies of CellProfiler, ensuring that the most memory- or disk-intensive steps aren't happening simultaneously, decreasing the likelihood of a crash. Read more about this and other configurations in [Step 1: Configuration](step_1_configuration.md). diff --git a/documentation/DS-documentation/step_0_prep.md b/documentation/DS-documentation/step_0_prep.md index 163c563..4b8fdba 100644 --- a/documentation/DS-documentation/step_0_prep.md +++ b/documentation/DS-documentation/step_0_prep.md @@ -31,7 +31,8 @@ In the current interface, it's easiest to click "Create role", select "EC2" from * [Create an SQS](http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/CreatingQueue.html) queue for unprocessable-messages to be dumped into (aka a DeadLetterQueue). ### 1.4 Primary Resources -The following five are resources you need to interact with constantly while working with Distributed-Something. Although at this point you don't need to create anything special there, you can open each console in a separate tab in your browser to keep them handy and monitor DCP's behavior. +The following five are resources you need to interact with constantly while working with Distributed-Something. +Although at this point you don't need to create anything special there, you can open each console in a separate tab in your browser to keep them handy and monitor DS's behavior. * [S3 Console](https://console.aws.amazon.com/s3) * [EC2 Console](https://console.aws.amazon.com/ec2/) * [ECS Console](https://console.aws.amazon.com/ecs/) @@ -58,7 +59,7 @@ Here we assume you are using the command line in a Linux machine, but you are fr You will need the scripts in Distributed-Something locally available in your control node.
     sudo apt-get install git
-    git clone https://github.com/cellprofiler/Distributed-Something.git
+    git clone https://github.com/DistributedScience/Distributed-Something.git
     cd Distributed-Something/
     git pull
 
diff --git a/documentation/DS-documentation/step_1_configuration.md b/documentation/DS-documentation/step_1_configuration.md index ba370cd..f481adf 100644 --- a/documentation/DS-documentation/step_1_configuration.md +++ b/documentation/DS-documentation/step_1_configuration.md @@ -53,7 +53,7 @@ This can safely be set to 0 for workflows that don't require much memory or exec * **SQS_MESSAGE_VISIBILITY:** How long each job is hidden from view before being allowed to be tried again. We recommend setting this to slightly longer than the average amount of time it takes an individual job to process- if you set it too short, you may waste resources doing the same job multiple times; if you set it too long, your instances may have to wait around a long while to access a job that was sent to an instance that stalled or has since been terminated. * **SQS_DEAD_LETTER_QUEUE:** The name of the queue to send jobs to if they fail to process correctly multiple times; this keeps a single bad job (such as one where a single file has been corrupted) from keeping your cluster active indefinitely. -See [[Setting up|Before-you-get-started:-setting-up]] for more information. +See [Step 0: Prep](step_0_prep.med) for more information. *** @@ -65,14 +65,12 @@ See [[Setting up|Before-you-get-started:-setting-up]] for more information. ### REDUNDANCY CHECKS +* **CHECK_IF_DONE_BOOL:** Whether or not to check the output folder before proceeding. +Case-insensitive. If an analysis fails partway through (due to some of the files being in the wrong place, an AWS outage, a machine crash, etc.), setting this to 'True' this allows you to resubmit the whole analysis but only reprocess jobs that haven't already been done. This saves you from having to try to parse exactly which jobs succeeded vs failed or from having to pay to rerun the entire analysis. If your software determines the correct number of files are already in the output folder it will designate that job as completed and move onto the next one. - If you actually do want to overwrite files that were previously generated (such as when you have improved a pipeline and no longer want the output of the old version), set this to 'False' to process jobs whether or not there are already files in the output folder. - -* **CHECK_IF_DONE_BOOL:** Whether or not to check the output folder before proceeding. -Case-insensitive. * **EXPECTED_NUMBER_FILES:** How many files need to be in the output folder in order to mark a job as completed. * **MIN_FILE_SIZE_BYTES:** What is the minimal number of bytes an object should be to "count"? Useful when trying to detect jobs that may have exported smaller corrupted files vs larger, full-size files. diff --git a/documentation/DS-documentation/step_3_start_cluster.md b/documentation/DS-documentation/step_3_start_cluster.md index b8a7663..2fe99a8 100644 --- a/documentation/DS-documentation/step_3_start_cluster.md +++ b/documentation/DS-documentation/step_3_start_cluster.md @@ -25,7 +25,7 @@ Looking at the output of this automatically generated spot fleet request can be Among the parameters you should/must update: -* **The IamFleetRole, IamInstanceProfile, KeyName, SubnetId, and Groups:** These are account specific and you will have configure these based on the [previous setup work that you did](step_0_prep.md). +* **The IamFleetRole, IamInstanceProfile, KeyName, SubnetId, and Groups:** These are account specific and you will configure these based on the [previous setup work that you did](step_0_prep.md). Once you've created your first complete spot fleet request, you can save a copy as a local template so that you don't have to look these up every time. * The KeyName used here should be the same used in your config file but **without** the `.pem` extension. diff --git a/documentation/DS-documentation/step_4_monitor.md b/documentation/DS-documentation/step_4_monitor.md index 7e9bbe1..6a0966f 100644 --- a/documentation/DS-documentation/step_4_monitor.md +++ b/documentation/DS-documentation/step_4_monitor.md @@ -7,7 +7,7 @@ Distributed-Something will keep an eye on a few things for you at this point wit * You can also look at the whole-cluster CPU and memory usage statistics related to your APP_NAME in the [ECS web interface](https://console.aws.amazon.com/ecs/home). -* Each instance will have an alarm placed on it so that if CPU usage to dips below 1% for 15 consecutive minutes (almost always the result of a crashed machine), the instance will be automatically terminated and a new one will take its place. +* Each instance will have an alarm placed on it so that if CPU usage dips below 1% for 15 consecutive minutes (almost always the result of a crashed machine), the instance will be automatically terminated and a new one will take its place. * Each individual job processed will create a log of the CellProfiler output, and each Docker container will create a log showing CPU, memory, and disk usage. @@ -24,7 +24,7 @@ The monitor can be run by entering `python run.py monitor files/APP_NAMESpotFlee * Checks your queue once per minute to see how many jobs are currently processing and how many remain to be processed. -* Once per day, it deletes the alarms for any instances that have been terminated in the last 24 hours (because of spot prices rising above your maximum bid, machine crashes, etc). +* Once per hour, it deletes the alarms for any instances that have been terminated in the last 24 hours (because of spot prices rising above your maximum bid, machine crashes, etc). ### When the number of jobs in your queue goes to 0 @@ -47,5 +47,5 @@ You can engage cheapest mode by adding `True` as a final configurable parameter Cheapest mode is cheapest because it will remove all but 1 machine as soon as that machine crashes and/or runs out of jobs to do; this can save you money, particularly in multi-CPU Dockers running long jobs. -This mode is optional though, because running this way involves some inherent risks- if machines stall out due to processing errors, they will not be replaced, meaning your job will take overall longer. -Additionally, if there is limited capacity for your requested configuration when you first start (aka you want 200 machines but AWS says it can currently only allocate you 50), more machines will not be added if and when they become available in cheapest mode as they would in normal mode. +This mode is optional because running this way involves some inherent risks- if machines stall out due to processing errors, they will not be replaced, meaning your job will take overall longer. +Additionally, if there is limited capacity for your requested configuration when you first start (e.g. you want 200 machines but AWS says it can currently only allocate you 50), more machines will not be added if and when they become available in cheapest mode as they would in normal mode. From b5d94175fe32375d0aefa4e2b7a0765bae8f14a8 Mon Sep 17 00:00:00 2001 From: ErinWeisbart Date: Tue, 30 Aug 2022 10:19:10 -0700 Subject: [PATCH 07/10] connect to docker daemon --- documentation/DS-documentation/implementing_DS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/documentation/DS-documentation/implementing_DS.md b/documentation/DS-documentation/implementing_DS.md index fca67d1..64f0e4b 100644 --- a/documentation/DS-documentation/implementing_DS.md +++ b/documentation/DS-documentation/implementing_DS.md @@ -12,6 +12,9 @@ For examples that we use in our Distributed-Something suite, you can refer to th Once you have made all the alterations to the Distributed-Something code detailed in [Customizing Distributed-Something](customizing_DS.md), you need to make your Distributed-Something Docker image. You will need a [DockerHub account](https://hub.docker.com). +Connect to the Docker daemon. +We find the simplest way to do this is to download and open Docker Desktop. +You can leave Docker Desktop open in the background while you continue to work at the command line.
 # Navigate into the Distributed-Something/worker folder

From 6681533cbcee5552a7d32984e000565327a73a2a Mon Sep 17 00:00:00 2001
From: ErinWeisbart 
Date: Tue, 30 Aug 2022 10:54:45 -0700
Subject: [PATCH 08/10] minor edits

---
 documentation/DS-documentation/_config.yml | 9 ---------
 documentation/DS-documentation/versions.md | 2 +-
 documentation/README.md                    | 6 +++---
 3 files changed, 4 insertions(+), 13 deletions(-)

diff --git a/documentation/DS-documentation/_config.yml b/documentation/DS-documentation/_config.yml
index e892891..36638eb 100644
--- a/documentation/DS-documentation/_config.yml
+++ b/documentation/DS-documentation/_config.yml
@@ -15,15 +15,6 @@ only_build_toc_files: true
 execute:
   execute_notebooks: force
 
-# Define the name of the latex output file for PDF builds
-#latex:
-#  latex_documents:
-#    targetname: book.tex
-
-# Add a bibtex file so that we can create citations
-# bibtex_bibfiles:
-#   - references.bib
-
 # Information about where the book exists on the web
 repository:
   url: https://github.com/distributed-something/documentation
diff --git a/documentation/DS-documentation/versions.md b/documentation/DS-documentation/versions.md
index 58137d5..092339b 100644
--- a/documentation/DS-documentation/versions.md
+++ b/documentation/DS-documentation/versions.md
@@ -1,6 +1,6 @@
 # Versions
 
-The most current release can always be found at `cellprofiler/distributed-something`.
+The most current release can always be found at `distributedscience/distributed-something`.
 Current version is 1.0.0.  
 
 ---
diff --git a/documentation/README.md b/documentation/README.md
index 3b7e0fe..4eef9a6 100644
--- a/documentation/README.md
+++ b/documentation/README.md
@@ -1,13 +1,13 @@
 This folder contains the files to automatically generate documentation for your Distributed-Something distribution using [Jupyter Book](https://jupyterbook.org/en/stable/intro.html).
 This documentation is linked from the main repository README and serves in place of a wiki.
 
-Instructions for customizing your Distributed-Something distribution are in the `CREATING DS` section and can be deleted from your repository's documentation you have completed creating your Distributed-Something distribution.
+Instructions for customizing your Distributed-Something distribution are in the `Creating DS` section and can be deleted from your repository's documentation you have completed creating your Distributed-Something distribution.
 
-Documentation for running Distributed-Something are in the `RUNNING DS` section and should be updated to fit your particular Distributed-Something implementation.
+Documentation for running Distributed-Something are in the `Running DS` section and should be updated to fit your particular Distributed-Something implementation.
 
 This documentation will automatically re-build any time you push to your repository's `main` branch.
 By default, only pages that have undergone edits will rebuild.
 
 To enable this auto-build, you need to set up a GitHub Action in your repository.
-Read more about [GitHub Actions](https://help.github.com/en/actions) and about [hosting Jupyter Books with GitHub Actions](https://jupyterbook.org/en/stable/publish/gh-pages.html?highlight=github#automatically-host-your-book-with-github-actions).
+Read more about [GitHub Actions](https://help.github.com/en/actions) and about [hosting Jupyter Books with GitHub Actions](https://jupyterbook.org/en/stable/publish/gh-pages.html#automatically-host-your-book-with-github-actions).
 It will auto-build the documentation to a `gh-pages` branch which will be visible at `githubusername.github.io/yourbookname`.

From 434c1a9f4ffbf6e148a1a549955fd38759fa07d9 Mon Sep 17 00:00:00 2001
From: ErinWeisbart 
Date: Tue, 30 Aug 2022 10:59:56 -0700
Subject: [PATCH 09/10] typo

---
 config.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/config.py b/config.py
index 957f39d..7d49abc 100644
--- a/config.py
+++ b/config.py
@@ -20,7 +20,7 @@
 EBS_VOL_SIZE = 30                       # In GB.  Minimum allowed is 22.
 
 # DOCKER INSTANCE RUNNING ENVIRONMENT:
-DOCKER_CORES = 4                        # Number of sofrware processes to run inside a docker container
+DOCKER_CORES = 4                        # Number of software processes to run inside a docker container
 CPU_SHARES = DOCKER_CORES * 1024        # ECS computing units assigned to each docker container (1024 units = 1 core)
 MEMORY = 15000                          # Memory assigned to the docker container in MB
 SECONDS_TO_START = 3*60                 # Wait before the next process is initiated to avoid memory collisions

From 030a71cfee0883a306b50eaae6da2af20d0cb111 Mon Sep 17 00:00:00 2001
From: ErinWeisbart 
Date: Tue, 30 Aug 2022 11:08:15 -0700
Subject: [PATCH 10/10] remove changes from this branch

---
 run.py                     | 673 ++++++++++++++++---------------------
 worker/Dockerfile          |  13 +-
 worker/generic-worker.py   | 242 ++++++-------
 worker/instance-monitor.py |  12 +-
 worker/run-worker.sh       |   8 +-
 5 files changed, 418 insertions(+), 530 deletions(-)

diff --git a/run.py b/run.py
index 52d36f0..32dc36f 100644
--- a/run.py
+++ b/run.py
@@ -1,3 +1,4 @@
+from __future__ import print_function
 import os, sys
 import boto3
 import datetime
@@ -9,7 +10,6 @@
 from email.mime.text import MIMEText
 
 from config import *
-
 WAIT_TIME = 60
 MONITOR_TIME = 60
 
@@ -22,7 +22,12 @@
     "family": APP_NAME,
     "containerDefinitions": [
         {
-            "environment": [{"name": "AWS_REGION", "value": AWS_REGION}],
+            "environment": [
+                {
+                    "name": "AWS_REGION",
+                    "value": AWS_REGION
+                }
+                ],
             "name": APP_NAME,
             "image": DOCKERHUB_TAG,
             "cpu": CPU_SHARES,
@@ -32,13 +37,13 @@
             "logConfiguration": {
                 "logDriver": "awslogs",
                 "options": {
-                    "awslogs-group": LOG_GROUP_NAME + "_perInstance",
+                    "awslogs-group": LOG_GROUP_NAME+"_perInstance",
                     "awslogs-region": AWS_REGION,
-                    "awslogs-stream-prefix": APP_NAME,
-                },
-            },
+                    "awslogs-stream-prefix": APP_NAME
+                    }
+                }
         }
-    ],
+    ]
 }
 
 SQS_DEFINITION = {
@@ -46,10 +51,8 @@
     "MaximumMessageSize": "262144",
     "MessageRetentionPeriod": "1209600",
     "ReceiveMessageWaitTimeSeconds": "0",
-    "RedrivePolicy": '{"deadLetterTargetArn":"'
-    + SQS_DEAD_LETTER_QUEUE
-    + '","maxReceiveCount":"10"}',
-    "VisibilityTimeout": str(SQS_MESSAGE_VISIBILITY),
+    "RedrivePolicy": "{\"deadLetterTargetArn\":\"" + SQS_DEAD_LETTER_QUEUE + "\",\"maxReceiveCount\":\"10\"}",
+    "VisibilityTimeout": str(SQS_MESSAGE_VISIBILITY)
 }
 
 
@@ -57,285 +60,256 @@
 # AUXILIARY FUNCTIONS
 #################################
 
-
 def get_aws_credentials(AWS_PROFILE):
     session = boto3.Session(profile_name=AWS_PROFILE)
     credentials = session.get_credentials()
     return credentials.access_key, credentials.secret_key
 
-
 def generate_task_definition(AWS_PROFILE):
     task_definition = TASK_DEFINITION.copy()
     key, secret = get_aws_credentials(AWS_PROFILE)
-    sqs = boto3.client("sqs")
+    sqs = boto3.client('sqs')
     queue_name = get_queue_url(sqs)
-    task_definition["containerDefinitions"][0]["environment"] += [
-        {"name": "APP_NAME", "value": APP_NAME},
-        {"name": "SQS_QUEUE_URL", "value": queue_name},
-        {"name": "AWS_ACCESS_KEY_ID", "value": key},
-        {"name": "AWS_SECRET_ACCESS_KEY", "value": secret},
-        {"name": "AWS_BUCKET", "value": AWS_BUCKET},
-        {"name": "DOCKER_CORES", "value": str(DOCKER_CORES)},
-        {"name": "LOG_GROUP_NAME", "value": LOG_GROUP_NAME},
-        {"name": "CHECK_IF_DONE_BOOL", "value": CHECK_IF_DONE_BOOL},
-        {"name": "EXPECTED_NUMBER_FILES", "value": str(EXPECTED_NUMBER_FILES)},
-        {"name": "ECS_CLUSTER", "value": ECS_CLUSTER},
-        {"name": "SECONDS_TO_START", "value": str(SECONDS_TO_START)},
-        {"name": "MIN_FILE_SIZE_BYTES", "value": str(MIN_FILE_SIZE_BYTES)},
-        {"name": "NECESSARY_STRING", "value": NECESSARY_STRING},
+    task_definition['containerDefinitions'][0]['environment'] += [
+        {
+            'name': 'APP_NAME',
+            'value': APP_NAME
+        },
+        {
+            'name': 'SQS_QUEUE_URL',
+            'value': queue_name
+        },
+        {
+            "name": "AWS_ACCESS_KEY_ID",
+            "value": key
+        },
+        {
+            "name": "AWS_SECRET_ACCESS_KEY",
+            "value": secret
+        },
+        {
+            "name": "AWS_BUCKET",
+            "value": AWS_BUCKET
+        },
+        {
+            "name": "DOCKER_CORES",
+            "value": str(DOCKER_CORES)
+        },
+        {
+            "name": "LOG_GROUP_NAME",
+            "value": LOG_GROUP_NAME
+        },
+        {
+            "name": "CHECK_IF_DONE_BOOL",
+            "value": CHECK_IF_DONE_BOOL
+        },
+        {
+            "name": "EXPECTED_NUMBER_FILES",
+            "value": str(EXPECTED_NUMBER_FILES)
+        },
+        {
+            "name": "ECS_CLUSTER",
+            "value": ECS_CLUSTER
+        },
+        {
+            "name": "SECONDS_TO_START",
+            "value": str(SECONDS_TO_START)
+        },
+        {
+            "name": "MIN_FILE_SIZE_BYTES",
+            "value": str(MIN_FILE_SIZE_BYTES)
+        },
+        {
+            "name": "NECESSARY_STRING",
+            "value": NECESSARY_STRING
+        }      
     ]
     return task_definition
 
-
 def update_ecs_task_definition(ecs, ECS_TASK_NAME, AWS_PROFILE):
     task_definition = generate_task_definition(AWS_PROFILE)
-    ecs.register_task_definition(
-        family=ECS_TASK_NAME,
-        containerDefinitions=task_definition["containerDefinitions"],
-    )
-    print("Task definition registered")
-
+    ecs.register_task_definition(family=ECS_TASK_NAME,containerDefinitions=task_definition['containerDefinitions'])
+    print('Task definition registered')
 
 def get_or_create_cluster(ecs):
     data = ecs.list_clusters()
-    cluster = [clu for clu in data["clusterArns"] if clu.endswith(ECS_CLUSTER)]
+    cluster = [clu for clu in data['clusterArns'] if clu.endswith(ECS_CLUSTER)]
     if len(cluster) == 0:
         ecs.create_cluster(clusterName=ECS_CLUSTER)
         time.sleep(WAIT_TIME)
-        print("Cluster " + ECS_CLUSTER + " created")
+        print('Cluster '+ECS_CLUSTER+' created')
     else:
-        print("Cluster " + ECS_CLUSTER + " exists")
-
+        print('Cluster '+ECS_CLUSTER+' exists')
 
 def create_or_update_ecs_service(ecs, ECS_SERVICE_NAME, ECS_TASK_NAME):
     # Create the service with no workers (0 desired count)
     data = ecs.list_services(cluster=ECS_CLUSTER)
-    service = [srv for srv in data["serviceArns"] if srv.endswith(ECS_SERVICE_NAME)]
+    service = [srv for srv in data['serviceArns'] if srv.endswith(ECS_SERVICE_NAME)]
     if len(service) > 0:
-        print("Service exists. Removing")
+        print('Service exists. Removing')
         ecs.delete_service(cluster=ECS_CLUSTER, service=ECS_SERVICE_NAME)
-        print("Removed service " + ECS_SERVICE_NAME)
+        print('Removed service '+ECS_SERVICE_NAME)
         time.sleep(WAIT_TIME)
 
-    print("Creating new service")
-    ecs.create_service(
-        cluster=ECS_CLUSTER,
-        serviceName=ECS_SERVICE_NAME,
-        taskDefinition=ECS_TASK_NAME,
-        desiredCount=0,
-    )
-    print("Service created")
-
+    print('Creating new service')
+    ecs.create_service(cluster=ECS_CLUSTER, serviceName=ECS_SERVICE_NAME, taskDefinition=ECS_TASK_NAME, desiredCount=0)
+    print('Service created')
 
 def get_queue_url(sqs):
     result = sqs.list_queues()
-    if "QueueUrls" in result.keys():
-        for u in result["QueueUrls"]:
-            if u.split("/")[-1] == SQS_QUEUE_NAME:
+    if 'QueueUrls' in result.keys():
+        for u in result['QueueUrls']:
+            if u.split('/')[-1] == SQS_QUEUE_NAME:
                 return u
     return None
 
-
 def get_or_create_queue(sqs):
     u = get_queue_url(sqs)
     if u is None:
-        print("Creating queue")
+        print('Creating queue')
         sqs.create_queue(QueueName=SQS_QUEUE_NAME, Attributes=SQS_DEFINITION)
         time.sleep(WAIT_TIME)
     else:
-        print("Queue exists")
-
+        print('Queue exists')
 
 def loadConfig(configFile):
     data = None
-    with open(configFile, "r") as conf:
+    with open(configFile, 'r') as conf:
         data = json.load(conf)
     return data
 
+def killdeadAlarms(fleetId,monitorapp,ec2,cloud):
+    todel=[]
+    changes = ec2.describe_spot_fleet_request_history(SpotFleetRequestId=fleetId,StartTime=(datetime.datetime.now()-datetime.timedelta(hours=2)).replace(microsecond=0))
+    for eachevent in changes['HistoryRecords']:
+        if eachevent['EventType']=='instanceChange':
+            if eachevent['EventInformation']['EventSubType']=='terminated':
+                todel.append(eachevent['EventInformation']['InstanceId'])
 
-def killdeadAlarms(fleetId, monitorapp, ec2, cloud):
-    todel = []
-    changes = ec2.describe_spot_fleet_request_history(
-        SpotFleetRequestId=fleetId,
-        StartTime=(datetime.datetime.now() - datetime.timedelta(hours=2)).replace(
-            microsecond=0
-        ),
-    )
-    for eachevent in changes["HistoryRecords"]:
-        if eachevent["EventType"] == "instanceChange":
-            if eachevent["EventInformation"]["EventSubType"] == "terminated":
-                todel.append(eachevent["EventInformation"]["InstanceId"])
-
-    existing_alarms = [
-        x["AlarmName"]
-        for x in cloud.describe_alarms(AlarmNamePrefix=monitorapp)["MetricAlarms"]
-    ]
-
+    existing_alarms = [x['AlarmName'] for x in cloud.describe_alarms(AlarmNamePrefix=monitorapp)['MetricAlarms']]
+    
     for eachmachine in todel:
-        monitorname = monitorapp + "_" + eachmachine
+        monitorname = monitorapp+'_'+eachmachine
         if monitorname in existing_alarms:
             cloud.delete_alarms(AlarmNames=[monitorname])
-            print("Deleted", monitorname, "if it existed")
+            print('Deleted', monitorname, 'if it existed')
             time.sleep(3)
+    
+    print('Old alarms deleted')
 
-    print("Old alarms deleted")
-
-
-def generateECSconfig(ECS_CLUSTER, APP_NAME, AWS_BUCKET, s3client):
-    configfile = open("configtemp.config", "w")
-    configfile.write("ECS_CLUSTER=" + ECS_CLUSTER + "\n")
+def generateECSconfig(ECS_CLUSTER,APP_NAME,AWS_BUCKET,s3client):
+    configfile=open('configtemp.config','w')
+    configfile.write('ECS_CLUSTER='+ECS_CLUSTER+'\n')
     configfile.write('ECS_AVAILABLE_LOGGING_DRIVERS=["json-file","awslogs"]')
     configfile.close()
-    s3client.upload_file(
-        "configtemp.config", AWS_BUCKET, "ecsconfigs/" + APP_NAME + "_ecs.config"
-    )
-    os.remove("configtemp.config")
-    return "s3://" + AWS_BUCKET + "/ecsconfigs/" + APP_NAME + "_ecs.config"
-
-
-def generateUserData(ecsConfigFile, dockerBaseSize):
-    config_str = "#!/bin/bash \n"
-    config_str += "sudo yum install -y aws-cli \n"
-    config_str += "sudo yum install -y awslogs \n"
-    config_str += "aws s3 cp " + ecsConfigFile + " /etc/ecs/ecs.config"
-
-    boothook_str = "#!/bin/bash \n"
-    boothook_str += (
-        "echo 'OPTIONS="
-        + '"${OPTIONS} --storage-opt dm.basesize='
-        + str(dockerBaseSize)
-        + 'G"'
-        + "' >> /etc/sysconfig/docker"
-    )
-
-    config = MIMEText(config_str, _subtype="x-shellscript")
-    config.add_header("Content-Disposition", "attachment", filename="config_temp.txt")
-
-    boothook = MIMEText(boothook_str, _subtype="cloud-boothook")
-    boothook.add_header(
-        "Content-Disposition", "attachment", filename="boothook_temp.txt"
-    )
+    s3client.upload_file('configtemp.config',AWS_BUCKET,'ecsconfigs/'+APP_NAME+'_ecs.config')
+    os.remove('configtemp.config')
+    return 's3://'+AWS_BUCKET+'/ecsconfigs/'+APP_NAME+'_ecs.config'
+
+def generateUserData(ecsConfigFile,dockerBaseSize):
+    config_str = '#!/bin/bash \n'
+    config_str += 'sudo yum install -y aws-cli \n'
+    config_str += 'sudo yum install -y awslogs \n'
+    config_str += 'aws s3 cp '+ecsConfigFile+' /etc/ecs/ecs.config'
+
+    boothook_str = '#!/bin/bash \n'
+    boothook_str += "echo 'OPTIONS="+'"${OPTIONS} --storage-opt dm.basesize='+str(dockerBaseSize)+'G"'+"' >> /etc/sysconfig/docker"
+
+    config = MIMEText(config_str, _subtype='x-shellscript')
+    config.add_header('Content-Disposition', 'attachment',filename='config_temp.txt')
+
+    boothook = MIMEText(boothook_str, _subtype='cloud-boothook')
+    boothook.add_header('Content-Disposition', 'attachment',filename='boothook_temp.txt')
 
     pre_user_data = MIMEMultipart()
     pre_user_data.attach(boothook)
     pre_user_data.attach(config)
 
-    try:  # Python2
+    try: #Python2
         return b64encode(pre_user_data.as_string())
-    except TypeError:  # Python3
+    except TypeError: #Python3
         pre_user_data_string = pre_user_data.as_string()
-        return b64encode(pre_user_data_string.encode("utf-8")).decode("utf-8")
-
+        return b64encode(pre_user_data_string.encode('utf-8')).decode('utf-8')
 
 def removequeue(queueName):
-    sqs = boto3.client("sqs")
-    queueoutput = sqs.list_queues(QueueNamePrefix=queueName)
-    if len(queueoutput["QueueUrls"]) == 1:
-        queueUrl = queueoutput["QueueUrls"][0]
-    else:  # In case we have "AnalysisQueue" and "AnalysisQueue1" and only want to delete the first of those
+    sqs = boto3.client('sqs')
+    queueoutput= sqs.list_queues(QueueNamePrefix=queueName)
+    if len(queueoutput["QueueUrls"])==1:
+        queueUrl=queueoutput["QueueUrls"][0]
+    else: #In case we have "AnalysisQueue" and "AnalysisQueue1" and only want to delete the first of those
         for eachUrl in queueoutput["QueueUrls"]:
-            if eachUrl.split("/")[-1] == queueName:
-                queueUrl = eachUrl
-
+            if eachUrl.split('/')[-1] == queueName:
+                queueUrl=eachUrl
+    
     sqs.delete_queue(QueueUrl=queueUrl)
 
-
 def deregistertask(taskName, ecs):
-    taskArns = ecs.list_task_definitions(familyPrefix=taskName, status="ACTIVE")
-    for eachtask in taskArns["taskDefinitionArns"]:
-        fulltaskname = eachtask.split("/")[-1]
+    taskArns = ecs.list_task_definitions(familyPrefix=taskName, status='ACTIVE')
+    for eachtask in taskArns['taskDefinitionArns']:
+        fulltaskname=eachtask.split('/')[-1]
         ecs.deregister_task_definition(taskDefinition=fulltaskname)
 
-
 def removeClusterIfUnused(clusterName, ecs):
-    if clusterName != "default":
-        # never delete the default cluster
+    if clusterName != 'default':
+        #never delete the default cluster
         result = ecs.describe_clusters(clusters=[clusterName])
-        if (
-            sum(
-                [
-                    result["clusters"][0]["pendingTasksCount"],
-                    result["clusters"][0]["runningTasksCount"],
-                    result["clusters"][0]["activeServicesCount"],
-                    result["clusters"][0]["registeredContainerInstancesCount"],
-                ]
-            )
-            == 0
-        ):
+        if sum([result['clusters'][0]['pendingTasksCount'],result['clusters'][0]['runningTasksCount'],result['clusters'][0]['activeServicesCount'],result['clusters'][0]['registeredContainerInstancesCount']])==0:
             ecs.delete_cluster(cluster=clusterName)
 
-
 def downscaleSpotFleet(queue, spotFleetID, ec2, manual=False):
     visible, nonvisible = queue.returnLoad()
     if manual:
-        ec2.modify_spot_fleet_request(
-            ExcessCapacityTerminationPolicy="noTermination",
-            SpotFleetRequestId=spotFleetID,
-            TargetCapacity=int(manual),
-        )
+        ec2.modify_spot_fleet_request(ExcessCapacityTerminationPolicy='noTermination', SpotFleetRequestId=spotFleetID, TargetCapacity = int(manual))
         return
     elif visible > 0:
         return
     else:
         status = ec2.describe_spot_fleet_instances(SpotFleetRequestId=spotFleetID)
-        if nonvisible < len(status["ActiveInstances"]):
-            ec2.modify_spot_fleet_request(
-                ExcessCapacityTerminationPolicy="noTermination",
-                SpotFleetRequestId=spotFleetID,
-                TargetCapacity=nonvisible,
-            )
-
+        if nonvisible < len(status['ActiveInstances']):
+            ec2.modify_spot_fleet_request(ExcessCapacityTerminationPolicy='noTermination', SpotFleetRequestId=spotFleetID, TargetCapacity = nonvisible)
 
 def export_logs(logs, loggroupId, starttime, bucketId):
-    result = logs.create_export_task(
-        taskName=loggroupId,
-        logGroupName=loggroupId,
-        fromTime=int(starttime),
-        to=int(time.time() * 1000),
-        destination=bucketId,
-        destinationPrefix="exportedlogs/" + loggroupId,
-    )
-
-    logExportId = result["taskId"]
+    result = logs.create_export_task(taskName = loggroupId, logGroupName = loggroupId, fromTime = int(starttime), to = int(time.time()*1000), destination = bucketId, destinationPrefix = 'exportedlogs/'+loggroupId)
+    
+    logExportId = result['taskId']
 
     while True:
-        result = logs.describe_export_tasks(taskId=logExportId)
-        if result["exportTasks"][0]["status"]["code"] != "PENDING":
-            if result["exportTasks"][0]["status"]["code"] != "RUNNING":
-                print(result["exportTasks"][0]["status"]["code"])
+        result = logs.describe_export_tasks(taskId = logExportId)
+        if result['exportTasks'][0]['status']['code']!='PENDING':
+            if result['exportTasks'][0]['status']['code']!='RUNNING':
+                print(result['exportTasks'][0]['status']['code'])
                 break
         time.sleep(30)
 
-
 #################################
 # CLASS TO HANDLE SQS QUEUE
 #################################
 
+class JobQueue():
 
-class JobQueue:
-    def __init__(self, name=None):
-        self.sqs = boto3.resource("sqs")
-        if name == None:
+    def __init__(self,name=None):
+        self.sqs = boto3.resource('sqs')
+        if name==None:
             self.queue = self.sqs.get_queue_by_name(QueueName=SQS_QUEUE_NAME)
         else:
             self.queue = self.sqs.get_queue_by_name(QueueName=name)
-        self.inProcess = -1
+        self.inProcess = -1 
         self.pending = -1
 
     def scheduleBatch(self, data):
         msg = json.dumps(data)
         response = self.queue.send_message(MessageBody=msg)
-        print("Batch sent. Message ID:", response.get("MessageId"))
+        print('Batch sent. Message ID:',response.get('MessageId'))
 
     def pendingLoad(self):
         self.queue.load()
-        visible = int(self.queue.attributes["ApproximateNumberOfMessages"])
-        nonVis = int(self.queue.attributes["ApproximateNumberOfMessagesNotVisible"])
-        if [visible, nonVis] != [self.pending, self.inProcess]:
+        visible = int( self.queue.attributes['ApproximateNumberOfMessages'] )
+        nonVis = int( self.queue.attributes['ApproximateNumberOfMessagesNotVisible'] )
+        if [visible, nonVis] != [self.pending,self.inProcess]:
             self.pending = visible
             self.inProcess = nonVis
             d = datetime.datetime.now()
-            print(d, "In process:", nonVis, "Pending", visible)
+            print(d,'In process:',nonVis,'Pending',visible)
         if visible + nonVis > 0:
             return True
         else:
@@ -343,8 +317,8 @@ def pendingLoad(self):
 
     def returnLoad(self):
         self.queue.load()
-        visible = int(self.queue.attributes["ApproximateNumberOfMessages"])
-        nonVis = int(self.queue.attributes["ApproximateNumberOfMessagesNotVisible"])
+        visible = int( self.queue.attributes['ApproximateNumberOfMessages'] )
+        nonVis = int( self.queue.attributes['ApproximateNumberOfMessagesNotVisible'] )
         return visible, nonVis
 
 
@@ -352,211 +326,163 @@ def returnLoad(self):
 # SERVICE 1: SETUP (formerly fab)
 #################################
 
-
 def setup():
-    ECS_TASK_NAME = APP_NAME + "Task"
-    ECS_SERVICE_NAME = APP_NAME + "Service"
-    USER = os.environ["HOME"].split("/")[-1]
-    AWS_CONFIG_FILE_NAME = os.environ["HOME"] + "/.aws/config"
-    AWS_CREDENTIAL_FILE_NAME = os.environ["HOME"] + "/.aws/credentials"
-    sqs = boto3.client("sqs")
+    ECS_TASK_NAME = APP_NAME + 'Task'
+    ECS_SERVICE_NAME = APP_NAME + 'Service'
+    USER = os.environ['HOME'].split('/')[-1]
+    AWS_CONFIG_FILE_NAME = os.environ['HOME'] + '/.aws/config'
+    AWS_CREDENTIAL_FILE_NAME = os.environ['HOME'] + '/.aws/credentials'
+    sqs = boto3.client('sqs')
     get_or_create_queue(sqs)
-    ecs = boto3.client("ecs")
+    ecs = boto3.client('ecs')
     get_or_create_cluster(ecs)
     update_ecs_task_definition(ecs, ECS_TASK_NAME, AWS_PROFILE)
     create_or_update_ecs_service(ecs, ECS_SERVICE_NAME, ECS_TASK_NAME)
 
-
 #################################
 # SERVICE 2: SUBMIT JOB
 #################################
 
-
 def submitJob():
     if len(sys.argv) < 3:
-        print("Use: run.py submitJob jobfile")
+        print('Use: run.py submitJob jobfile')
         sys.exit()
 
     # Step 1: Read the job configuration file
     jobInfo = loadConfig(sys.argv[2])
-    templateMessage = {
-        eachkey: jobInfo[eachkey]
-        for eachkey in jobInfo.keys()
-        if eachkey != "groups" and "_comment" not in eachkey
-    }
+    templateMessage = {eachkey:jobInfo[eachkey] for eachkey in jobInfo.keys() if eachkey != "groups" and "_comment" not in eachkey}
 
     # Step 2: Reach the queue and schedule tasks
-    print("Contacting queue")
+    print('Contacting queue')
     queue = JobQueue()
-    print("Scheduling tasks")
+    print('Scheduling tasks')
     for batch in jobInfo["groups"]:
         templateMessage["group"] = batch
         queue.scheduleBatch(templateMessage)
-    print("Job submitted. Check your queue")
-
+    print('Job submitted. Check your queue')
 
 #################################
-# SERVICE 3: START CLUSTER
+# SERVICE 3: START CLUSTER 
 #################################
 
-
 def startCluster():
     if len(sys.argv) < 3:
-        print("Use: run.py startCluster configFile")
+        print('Use: run.py startCluster configFile')
         sys.exit()
 
     thistime = datetime.datetime.now().replace(microsecond=0)
-    # Step 1: set up the configuration files
-    s3client = boto3.client("s3")
-    ecsConfigFile = generateECSconfig(ECS_CLUSTER, APP_NAME, AWS_BUCKET, s3client)
-    spotfleetConfig = loadConfig(sys.argv[2])
-    spotfleetConfig["ValidFrom"] = thistime
-    spotfleetConfig["ValidUntil"] = (thistime + datetime.timedelta(days=365)).replace(
-        microsecond=0
-    )
-    spotfleetConfig["TargetCapacity"] = CLUSTER_MACHINES
-    spotfleetConfig["SpotPrice"] = "%.2f" % MACHINE_PRICE
-    DOCKER_BASE_SIZE = int(round(float(EBS_VOL_SIZE) / int(TASKS_PER_MACHINE))) - 2
-    userData = generateUserData(ecsConfigFile, DOCKER_BASE_SIZE)
-    for LaunchSpecification in range(0, len(spotfleetConfig["LaunchSpecifications"])):
-        spotfleetConfig["LaunchSpecifications"][LaunchSpecification][
-            "UserData"
-        ] = userData
-        spotfleetConfig["LaunchSpecifications"][LaunchSpecification][
-            "BlockDeviceMappings"
-        ][1]["Ebs"]["VolumeSize"] = EBS_VOL_SIZE
-        spotfleetConfig["LaunchSpecifications"][LaunchSpecification][
-            "InstanceType"
-        ] = MACHINE_TYPE[LaunchSpecification]
+    #Step 1: set up the configuration files
+    s3client = boto3.client('s3')
+    ecsConfigFile=generateECSconfig(ECS_CLUSTER,APP_NAME,AWS_BUCKET,s3client)
+    spotfleetConfig=loadConfig(sys.argv[2])
+    spotfleetConfig['ValidFrom']=thistime
+    spotfleetConfig['ValidUntil']=(thistime+datetime.timedelta(days=365)).replace(microsecond=0)
+    spotfleetConfig['TargetCapacity']= CLUSTER_MACHINES
+    spotfleetConfig['SpotPrice'] = '%.2f' %MACHINE_PRICE
+    DOCKER_BASE_SIZE = int(round(float(EBS_VOL_SIZE)/int(TASKS_PER_MACHINE))) - 2
+    userData=generateUserData(ecsConfigFile,DOCKER_BASE_SIZE)
+    for LaunchSpecification in range(0,len(spotfleetConfig['LaunchSpecifications'])): 
+        spotfleetConfig['LaunchSpecifications'][LaunchSpecification]["UserData"]=userData
+        spotfleetConfig['LaunchSpecifications'][LaunchSpecification]['BlockDeviceMappings'][1]['Ebs']["VolumeSize"]= EBS_VOL_SIZE
+        spotfleetConfig['LaunchSpecifications'][LaunchSpecification]['InstanceType'] = MACHINE_TYPE[LaunchSpecification]
+
 
     # Step 2: make the spot fleet request
-    ec2client = boto3.client("ec2")
+    ec2client=boto3.client('ec2')
     requestInfo = ec2client.request_spot_fleet(SpotFleetRequestConfig=spotfleetConfig)
-    print("Request in process. Wait until your machines are available in the cluster.")
-    print("SpotFleetRequestId", requestInfo["SpotFleetRequestId"])
+    print('Request in process. Wait until your machines are available in the cluster.')
+    print('SpotFleetRequestId',requestInfo['SpotFleetRequestId'])
 
     # Step 3: Make the monitor
-    starttime = str(int(time.time() * 1000))
-    createMonitor = open("files/" + APP_NAME + "SpotFleetRequestId.json", "w")
-    createMonitor.write(
-        '{"MONITOR_FLEET_ID" : "' + requestInfo["SpotFleetRequestId"] + '",\n'
-    )
-    createMonitor.write('"MONITOR_APP_NAME" : "' + APP_NAME + '",\n')
-    createMonitor.write('"MONITOR_ECS_CLUSTER" : "' + ECS_CLUSTER + '",\n')
-    createMonitor.write('"MONITOR_QUEUE_NAME" : "' + SQS_QUEUE_NAME + '",\n')
-    createMonitor.write('"MONITOR_BUCKET_NAME" : "' + AWS_BUCKET + '",\n')
-    createMonitor.write('"MONITOR_LOG_GROUP_NAME" : "' + LOG_GROUP_NAME + '",\n')
-    createMonitor.write('"MONITOR_START_TIME" : "' + starttime + '"}\n')
+    starttime=str(int(time.time()*1000))
+    createMonitor=open('files/' + APP_NAME + 'SpotFleetRequestId.json','w')
+    createMonitor.write('{"MONITOR_FLEET_ID" : "'+requestInfo['SpotFleetRequestId']+'",\n')
+    createMonitor.write('"MONITOR_APP_NAME" : "'+APP_NAME+'",\n')
+    createMonitor.write('"MONITOR_ECS_CLUSTER" : "'+ECS_CLUSTER+'",\n')
+    createMonitor.write('"MONITOR_QUEUE_NAME" : "'+SQS_QUEUE_NAME+'",\n')
+    createMonitor.write('"MONITOR_BUCKET_NAME" : "'+AWS_BUCKET+'",\n')
+    createMonitor.write('"MONITOR_LOG_GROUP_NAME" : "'+LOG_GROUP_NAME+'",\n')
+    createMonitor.write('"MONITOR_START_TIME" : "'+ starttime+'"}\n')
     createMonitor.close()
-
+    
     # Step 4: Create a log group for this app and date if one does not already exist
-    logclient = boto3.client("logs")
-    loggroupinfo = logclient.describe_log_groups(logGroupNamePrefix=LOG_GROUP_NAME)
-    groupnames = [d["logGroupName"] for d in loggroupinfo["logGroups"]]
+    logclient=boto3.client('logs')
+    loggroupinfo=logclient.describe_log_groups(logGroupNamePrefix=LOG_GROUP_NAME)
+    groupnames=[d['logGroupName'] for d in loggroupinfo['logGroups']]
     if LOG_GROUP_NAME not in groupnames:
         logclient.create_log_group(logGroupName=LOG_GROUP_NAME)
         logclient.put_retention_policy(logGroupName=LOG_GROUP_NAME, retentionInDays=60)
-    if LOG_GROUP_NAME + "_perInstance" not in groupnames:
-        logclient.create_log_group(logGroupName=LOG_GROUP_NAME + "_perInstance")
-        logclient.put_retention_policy(
-            logGroupName=LOG_GROUP_NAME + "_perInstance", retentionInDays=60
-        )
-
+    if LOG_GROUP_NAME+'_perInstance' not in groupnames:
+        logclient.create_log_group(logGroupName=LOG_GROUP_NAME+'_perInstance')
+        logclient.put_retention_policy(logGroupName=LOG_GROUP_NAME+'_perInstance', retentionInDays=60)
+    
     # Step 5: update the ECS service to be ready to inject docker containers in EC2 instances
-    print("Updating service")
-    ecs = boto3.client("ecs")
-    ecs.update_service(
-        cluster=ECS_CLUSTER,
-        service=APP_NAME + "Service",
-        desiredCount=CLUSTER_MACHINES * TASKS_PER_MACHINE,
-    )
-    print("Service updated.")
-
+    print('Updating service')
+    ecs = boto3.client('ecs')
+    ecs.update_service(cluster=ECS_CLUSTER, service=APP_NAME+'Service', desiredCount=CLUSTER_MACHINES*TASKS_PER_MACHINE)
+    print('Service updated.') 
+    
     # Step 6: Monitor the creation of the instances until all are present
-    status = ec2client.describe_spot_fleet_instances(
-        SpotFleetRequestId=requestInfo["SpotFleetRequestId"]
-    )
-    # time.sleep(15) # This is now too fast, so sometimes the spot fleet request history throws an error!
-    while len(status["ActiveInstances"]) < CLUSTER_MACHINES:
+    status = ec2client.describe_spot_fleet_instances(SpotFleetRequestId=requestInfo['SpotFleetRequestId'])
+    #time.sleep(15) # This is now too fast, so sometimes the spot fleet request history throws an error!
+    while len(status['ActiveInstances']) < CLUSTER_MACHINES:
         # First check to make sure there's not a problem
-        errorcheck = ec2client.describe_spot_fleet_request_history(
-            SpotFleetRequestId=requestInfo["SpotFleetRequestId"],
-            EventType="error",
-            StartTime=thistime - datetime.timedelta(minutes=1),
-        )
-        if len(errorcheck["HistoryRecords"]) != 0:
-            print(
-                "Your spot fleet request is causing an error and is now being cancelled.  Please check your configuration and try again"
-            )
-            for eacherror in errorcheck["HistoryRecords"]:
-                print(
-                    eacherror["EventInformation"]["EventSubType"]
-                    + " : "
-                    + eacherror["EventInformation"]["EventDescription"]
-                )
-            # If there's only one error, and it's the type we see for insufficient capacity (but also other types)
-            # AND if there are some machines on, indicating that other than capacity the spec is otherwise good, don't cancel
-            if len(errorcheck["HistoryRecords"]) == 1:
-                if (
-                    errorcheck["HistoryRecords"][0]["EventInformation"]["EventSubType"]
-                    == "allLaunchSpecsTemporarilyBlacklisted"
-                ):
-                    if len(status["ActiveInstances"]) >= 1:
-                        print(
-                            "I think, but am not sure, that this is an insufficient capacity error. You should check the console for more information."
-                        )
+        errorcheck = ec2client.describe_spot_fleet_request_history(SpotFleetRequestId=requestInfo['SpotFleetRequestId'], EventType='error', StartTime=thistime - datetime.timedelta(minutes=1))
+        if len(errorcheck['HistoryRecords']) != 0:
+            print('Your spot fleet request is causing an error and is now being cancelled.  Please check your configuration and try again')
+            for eacherror in errorcheck['HistoryRecords']:
+                print(eacherror['EventInformation']['EventSubType'] + ' : ' + eacherror['EventInformation']['EventDescription'])
+            #If there's only one error, and it's the type we see for insufficient capacity (but also other types)
+            #AND if there are some machines on, indicating that other than capacity the spec is otherwise good, don't cancel
+            if len(errorcheck['HistoryRecords']) == 1:
+                if errorcheck['HistoryRecords'][0]['EventInformation']['EventSubType'] == 'allLaunchSpecsTemporarilyBlacklisted':
+                    if len(status['ActiveInstances']) >= 1:
+                        print("I think, but am not sure, that this is an insufficient capacity error. You should check the console for more information.")
                         return
-            ec2client.cancel_spot_fleet_requests(
-                SpotFleetRequestIds=[requestInfo["SpotFleetRequestId"]],
-                TerminateInstances=True,
-            )
+            ec2client.cancel_spot_fleet_requests(SpotFleetRequestIds=[requestInfo['SpotFleetRequestId']], TerminateInstances=True)
             return
-
+        
         # If everything seems good, just bide your time until you're ready to go
-        print(".")
+        print('.')
         time.sleep(20)
-        status = ec2client.describe_spot_fleet_instances(
-            SpotFleetRequestId=requestInfo["SpotFleetRequestId"]
-        )
-
-    print("Spot fleet successfully created. Your job should start in a few minutes.")
+        status = ec2client.describe_spot_fleet_instances(SpotFleetRequestId=requestInfo['SpotFleetRequestId'])
 
+    print('Spot fleet successfully created. Your job should start in a few minutes.')
 
 #################################
-# SERVICE 4: MONITOR JOB
+# SERVICE 4: MONITOR JOB 
 #################################
 
-
 def monitor(cheapest=False):
     if len(sys.argv) < 3:
-        print("Use: run.py monitor spotFleetIdFile")
+        print('Use: run.py monitor spotFleetIdFile')
         sys.exit()
-
-    if ".json" not in sys.argv[2]:
-        print("Use: run.py monitor spotFleetIdFile")
+    
+    if '.json' not in sys.argv[2]:
+        print('Use: run.py monitor spotFleetIdFile')
         sys.exit()
 
     if len(sys.argv) == 4:
         cheapest = sys.argv[3]
-
+    
     monitorInfo = loadConfig(sys.argv[2])
-    monitorcluster = monitorInfo["MONITOR_ECS_CLUSTER"]
-    monitorapp = monitorInfo["MONITOR_APP_NAME"]
-    fleetId = monitorInfo["MONITOR_FLEET_ID"]
-    queueId = monitorInfo["MONITOR_QUEUE_NAME"]
+    monitorcluster=monitorInfo["MONITOR_ECS_CLUSTER"]
+    monitorapp=monitorInfo["MONITOR_APP_NAME"]
+    fleetId=monitorInfo["MONITOR_FLEET_ID"]
+    queueId=monitorInfo["MONITOR_QUEUE_NAME"]
 
-    ec2 = boto3.client("ec2")
-    cloud = boto3.client("cloudwatch")
+    ec2 = boto3.client('ec2')
+    cloud = boto3.client('cloudwatch') 
 
     # Optional Step 0 - decide if you're going to be cheap rather than fast. This means that you'll get 15 minutes
     # from the start of the monitor to get as many machines as you get, and then it will set the requested number to 1.
-    # Benefit: this will always be the cheapest possible way to run, because if machines die they'll die fast,
-    # Potential downside- if machines are at low availability when you start to run, you'll only ever get a small number
+    # Benefit: this will always be the cheapest possible way to run, because if machines die they'll die fast, 
+    # Potential downside- if machines are at low availability when you start to run, you'll only ever get a small number 
     # of machines (as opposed to getting more later when they become available), so it might take VERY long to run if that happens.
     if cheapest:
         queue = JobQueue(name=queueId)
         startcountdown = time.time()
-        while queue.pendingLoad():
+        while queue.pendingLoad(): 
             if time.time() - startcountdown > 900:
                 downscaleSpotFleet(queue, fleetId, ec2, manual=1)
                 break
@@ -565,101 +491,96 @@ def monitor(cheapest=False):
     # Step 1: Create job and count messages periodically
     queue = JobQueue(name=queueId)
     while queue.pendingLoad():
-        # Once an hour (except at midnight) check for terminated machines and delete their alarms.
-        # This is slooooooow, which is why we don't just do it at the end
-        curtime = datetime.datetime.now().strftime("%H%M")
-        if curtime[-2:] == "00":
-            if curtime[:2] != "00":
-                killdeadAlarms(fleetId, monitorapp, ec2, cloud)
-        # Once every 10 minutes, check if all jobs are in process, and if so scale the spot fleet size to match
-        # the number of jobs still in process WITHOUT force terminating them.
-        # This can help keep costs down if, for example, you start up 100+ machines to run a large job, and
-        # 1-10 jobs with errors are keeping it rattling around for hours.
-        if curtime[-1:] == "9":
+        #Once an hour (except at midnight) check for terminated machines and delete their alarms.  
+        #This is slooooooow, which is why we don't just do it at the end
+        curtime=datetime.datetime.now().strftime('%H%M')
+        if curtime[-2:]=='00':
+            if curtime[:2]!='00':
+                killdeadAlarms(fleetId,monitorapp,ec2,cloud)
+        #Once every 10 minutes, check if all jobs are in process, and if so scale the spot fleet size to match
+        #the number of jobs still in process WITHOUT force terminating them.
+        #This can help keep costs down if, for example, you start up 100+ machines to run a large job, and
+        #1-10 jobs with errors are keeping it rattling around for hours.
+        if curtime[-1:]=='9':
             downscaleSpotFleet(queue, fleetId, ec2)
         time.sleep(MONITOR_TIME)
-
+    
     # Step 2: When no messages are pending, stop service
     # Reload the monitor info, because for long jobs new fleets may have been started, etc
     monitorInfo = loadConfig(sys.argv[2])
-    monitorcluster = monitorInfo["MONITOR_ECS_CLUSTER"]
-    monitorapp = monitorInfo["MONITOR_APP_NAME"]
-    fleetId = monitorInfo["MONITOR_FLEET_ID"]
-    queueId = monitorInfo["MONITOR_QUEUE_NAME"]
-    bucketId = monitorInfo["MONITOR_BUCKET_NAME"]
-    loggroupId = monitorInfo["MONITOR_LOG_GROUP_NAME"]
-    starttime = monitorInfo["MONITOR_START_TIME"]
-
-    ecs = boto3.client("ecs")
-    ecs.update_service(
-        cluster=monitorcluster, service=monitorapp + "Service", desiredCount=0
-    )
-    print("Service has been downscaled")
+    monitorcluster=monitorInfo["MONITOR_ECS_CLUSTER"]
+    monitorapp=monitorInfo["MONITOR_APP_NAME"]
+    fleetId=monitorInfo["MONITOR_FLEET_ID"]
+    queueId=monitorInfo["MONITOR_QUEUE_NAME"]
+    bucketId=monitorInfo["MONITOR_BUCKET_NAME"]
+    loggroupId=monitorInfo["MONITOR_LOG_GROUP_NAME"]
+    starttime=monitorInfo["MONITOR_START_TIME"]    
+
+    ecs = boto3.client('ecs')
+    ecs.update_service(cluster=monitorcluster, service=monitorapp+'Service', desiredCount=0)
+    print('Service has been downscaled')
 
     # Step3: Delete the alarms from active machines and machines that have died since the last sweep
     # This is in a try loop, because while it is important, we don't want to not stop the spot fleet
     try:
         result = ec2.describe_spot_fleet_instances(SpotFleetRequestId=fleetId)
-        instancelist = result["ActiveInstances"]
+        instancelist = result['ActiveInstances']
         while len(instancelist) > 0:
             to_del = instancelist[:100]
-            del_alarms = [monitorapp + "_" + x["InstanceId"] for x in to_del]
+            del_alarms = [monitorapp+'_'+x['InstanceId'] for x in to_del]
             cloud.delete_alarms(AlarmNames=del_alarms)
             time.sleep(10)
             instancelist = instancelist[100:]
-        killdeadAlarms(fleetId, monitorapp)
+        killdeadAlarms(fleetId,monitorapp)
     except:
         pass
 
     # Step 4: Read spot fleet id and terminate all EC2 instances
-    print("Shutting down spot fleet", fleetId)
-    ec2.cancel_spot_fleet_requests(
-        SpotFleetRequestIds=[fleetId], TerminateInstances=True
-    )
-    print("Job done.")
+    print('Shutting down spot fleet',fleetId)
+    ec2.cancel_spot_fleet_requests(SpotFleetRequestIds=[fleetId], TerminateInstances=True)
+    print('Job done.')
 
     # Step 5. Release other resources
     # Remove SQS queue, ECS Task Definition, ECS Service
-    ECS_TASK_NAME = monitorapp + "Task"
-    ECS_SERVICE_NAME = monitorapp + "Service"
-    print("Deleting existing queue.")
+    ECS_TASK_NAME = monitorapp + 'Task'
+    ECS_SERVICE_NAME = monitorapp + 'Service'
+    print('Deleting existing queue.')
     removequeue(queueId)
-    print("Deleting service")
-    ecs.delete_service(cluster=monitorcluster, service=ECS_SERVICE_NAME)
-    print("De-registering task")
-    deregistertask(ECS_TASK_NAME, ecs)
+    print('Deleting service')
+    ecs.delete_service(cluster=monitorcluster, service = ECS_SERVICE_NAME)
+    print('De-registering task')
+    deregistertask(ECS_TASK_NAME,ecs)
     print("Removing cluster if it's not the default and not otherwise in use")
     removeClusterIfUnused(monitorcluster, ecs)
 
-    # Step 6: Export the logs to S3
-    logs = boto3.client("logs")
+    #Step 6: Export the logs to S3
+    logs=boto3.client('logs')
 
-    print("Transfer of program logs to S3 initiated")
+    print('Transfer of program logs to S3 initiated')
     export_logs(logs, loggroupId, starttime, bucketId)
 
-    print("Transfer of per-instance logs to S3 initiated")
-    export_logs(logs, loggroupId + "_perInstance", starttime, bucketId)
-
-    print("All export tasks done")
+    print('Transfer of per-instance logs to S3 initiated')
+    export_logs(logs, loggroupId+'_perInstance', starttime, bucketId)
 
+    print('All export tasks done')
 
 #################################
-# MAIN USER INTERACTION
+# MAIN USER INTERACTION 
 #################################
 
-if __name__ == "__main__":
+if __name__ == '__main__':
     if len(sys.argv) < 2:
-        print("Use: run.py setup | submitJob | startCluster | monitor")
+        print('Use: run.py setup | submitJob | startCluster | monitor')
         sys.exit()
-
-    if sys.argv[1] == "setup":
+    
+    if sys.argv[1] == 'setup':
         setup()
-    elif sys.argv[1] == "submitJob":
+    elif sys.argv[1] == 'submitJob':
         submitJob()
-    elif sys.argv[1] == "startCluster":
+    elif sys.argv[1] == 'startCluster':
         startCluster()
-    elif sys.argv[1] == "monitor":
+    elif sys.argv[1] == 'monitor':
         monitor()
     else:
-        print("Use: run.py setup | submitJob | startCluster | monitor")
+        print('Use: run.py setup | submitJob | startCluster | monitor')
         sys.exit()
diff --git a/worker/Dockerfile b/worker/Dockerfile
index 4728ae4..da3ab4e 100644
--- a/worker/Dockerfile
+++ b/worker/Dockerfile
@@ -2,13 +2,13 @@
 #                                 - [ BROAD'16 ] -
 #
 # A docker instance for accessing AWS resources
-# This wraps your custom docker registry
+# This wraps the cellprofiler docker registry
 #
 
 
 FROM someuser/somedocker:sometag
 
-
+# Install S3FS 
 
 RUN apt-get -y update           && \
     apt-get -y upgrade          && \
@@ -25,7 +25,6 @@ RUN apt-get -y update           && \
 	sysstat			\
 	curl
 
-# Install S3FS
 WORKDIR /usr/local/src
 RUN git clone https://github.com/s3fs-fuse/s3fs-fuse.git
 WORKDIR /usr/local/src/s3fs-fuse
@@ -36,18 +35,23 @@ RUN make install
 
 # Install Python - not needed if you've already got it in your container
 # If you have a non-3.8 version, you will need to change python3.8 calls where specified
+
 RUN apt install -y python3.8-dev python3.8-distutils
 
 # Install AWS CLI
-RUN python3.8 -m pip install awscli
+
+RUN python3.8 -m pip install awscli 
 
 # Install boto3
+
 RUN python3.8 -m pip install boto3
 
 # Install watchtower for logging
+
 RUN python3.8 -m pip install watchtower
 
 # SETUP NEW ENTRYPOINT
+
 RUN mkdir -p /home/ubuntu/
 WORKDIR /home/ubuntu
 COPY generic-worker.py .
@@ -58,3 +62,4 @@ RUN chmod 755 run-worker.sh
 WORKDIR /home/ubuntu
 ENTRYPOINT ["./run-worker.sh"]
 CMD [""]
+
diff --git a/worker/generic-worker.py b/worker/generic-worker.py
index b4d7c7f..63706f7 100644
--- a/worker/generic-worker.py
+++ b/worker/generic-worker.py
@@ -1,60 +1,62 @@
+from __future__ import print_function
 import boto3
+import glob
 import json
 import logging
 import os
+import re
 import subprocess
+import sys 
 import time
 import watchtower
+import string
 
 #################################
 # CONSTANT PATHS IN THE CONTAINER
 #################################
 
-DATA_ROOT = "/home/ubuntu/bucket"
-LOCAL_OUTPUT = "/home/ubuntu/local_output"
-QUEUE_URL = os.environ["SQS_QUEUE_URL"]
-AWS_BUCKET = os.environ["AWS_BUCKET"]
-LOG_GROUP_NAME = os.environ["LOG_GROUP_NAME"]
-CHECK_IF_DONE_BOOL = os.environ["CHECK_IF_DONE_BOOL"]
-EXPECTED_NUMBER_FILES = os.environ["EXPECTED_NUMBER_FILES"]
-if "MIN_FILE_SIZE_BYTES" not in os.environ:
+DATA_ROOT = '/home/ubuntu/bucket'
+LOCAL_OUTPUT = '/home/ubuntu/local_output'
+QUEUE_URL = os.environ['SQS_QUEUE_URL']
+AWS_BUCKET = os.environ['AWS_BUCKET']
+LOG_GROUP_NAME= os.environ['LOG_GROUP_NAME']
+CHECK_IF_DONE_BOOL= os.environ['CHECK_IF_DONE_BOOL']
+EXPECTED_NUMBER_FILES= os.environ['EXPECTED_NUMBER_FILES']
+if 'MIN_FILE_SIZE_BYTES' not in os.environ:
     MIN_FILE_SIZE_BYTES = 1
 else:
-    MIN_FILE_SIZE_BYTES = int(os.environ["MIN_FILE_SIZE_BYTES"])
-if "USE_PLUGINS" not in os.environ:
-    USE_PLUGINS = "False"
+    MIN_FILE_SIZE_BYTES = int(os.environ['MIN_FILE_SIZE_BYTES'])
+if 'USE_PLUGINS' not in os.environ:
+    USE_PLUGINS = 'False'
 else:
-    USE_PLUGINS = os.environ["USE_PLUGINS"]
-if "NECESSARY_STRING" not in os.environ:
+    USE_PLUGINS = os.environ['USE_PLUGINS']
+if 'NECESSARY_STRING' not in os.environ:
     NECESSARY_STRING = False
 else:
-    NECESSARY_STRING = os.environ["NECESSARY_STRING"]
-if "DOWNLOAD_FILES" not in os.environ:
+    NECESSARY_STRING = os.environ['NECESSARY_STRING']
+if 'DOWNLOAD_FILES' not in os.environ:
     DOWNLOAD_FILES = False
 else:
-    DOWNLOAD_FILES = os.environ["DOWNLOAD_FILES"]
-# If you added more system variables to config.py, enter them here
+    DOWNLOAD_FILES = os.environ['DOWNLOAD_FILES']
 
-localIn = "/home/ubuntu/local_input"
+localIn = '/home/ubuntu/local_input'
 
 
 #################################
 # CLASS TO HANDLE THE SQS QUEUE
 #################################
 
+class JobQueue():
 
-class JobQueue:
     def __init__(self, queueURL):
-        self.client = boto3.client("sqs")
+        self.client = boto3.client('sqs')
         self.queueURL = queueURL
-
+    
     def readMessage(self):
-        response = self.client.receive_message(
-            QueueUrl=self.queueURL, WaitTimeSeconds=20
-        )
-        if "Messages" in response.keys():
-            data = json.loads(response["Messages"][0]["Body"])
-            handle = response["Messages"][0]["ReceiptHandle"]
+        response = self.client.receive_message(QueueUrl=self.queueURL, WaitTimeSeconds=20)
+        if 'Messages' in response.keys():
+            data = json.loads(response['Messages'][0]['Body'])
+            handle = response['Messages'][0]['ReceiptHandle']
             return data, handle
         else:
             return None, None
@@ -64,168 +66,130 @@ def deleteMessage(self, handle):
         return
 
     def returnMessage(self, handle):
-        self.client.change_message_visibility(
-            QueueUrl=self.queueURL, ReceiptHandle=handle, VisibilityTimeout=60
-        )
+        self.client.change_message_visibility(QueueUrl=self.queueURL, ReceiptHandle=handle, VisibilityTimeout=60)
         return
 
-
 #################################
 # AUXILIARY FUNCTIONS
 #################################
 
 
-def monitorAndLog(process, logger):
+def monitorAndLog(process,logger):
     while True:
-        output = process.stdout.readline().decode()
-        if output == "" and process.poll() is not None:
+        output= process.stdout.readline().decode()
+        if output== '' and process.poll() is not None:
             break
         if output:
             print(output.strip())
-            logger.info(output)
-
+            logger.info(output)  
 
-def printandlog(text, logger):
+def printandlog(text,logger):
     print(text)
     logger.info(text)
 
-
 #################################
 # RUN SOME PROCESS
 #################################
 
-
 def runSomething(message):
-    # List the directories in the bucket- this prevents a strange S3Fs error
-    # You can remove this if you are not mounting S3FS
-    rootlist = os.listdir(DATA_ROOT)
+    #List the directories in the bucket- this prevents a strange s3fs error
+    rootlist=os.listdir(DATA_ROOT)
     for eachSubDir in rootlist:
-        subDirName = os.path.join(DATA_ROOT, eachSubDir)
+        subDirName=os.path.join(DATA_ROOT,eachSubDir)
         if os.path.isdir(subDirName):
-            trashvar = os.system("ls " + subDirName)
+            trashvar=os.system('ls '+subDirName)
 
     # Configure the logs
     logger = logging.getLogger(__name__)
 
-    # Parse your message to pull out a name variable to use for logging
-    # To include all group keys in your log name, use the commented-out code below
-    # Otherwise, create your own definition of metadataID
-    # group_to_run = message["group"]
-    # groupkeys = list(group_to_run.keys())
-    # groupkeys.sort()
-    # metadataID = '-'.join(groupkeys)
-
-    # Add a handler with
-    watchtowerlogger = watchtower.CloudWatchLogHandler(
-        log_group=LOG_GROUP_NAME, stream_name=str(metadataID), create_log_group=False
-    )
-    logger.addHandler(watchtowerlogger)
-
-    # See if this is a message you've already handled, if you've so chosen
-    # First, build a variable called remoteOut that equals your unique prefix of where your output should be
-    # e.g remoteOut = os.path.join(message['output'], metadataID)
-
+    # Parse your message somehow to pull out a name variable that's going to make sense to you when you want to look at the logs later
+    # What's commented out below will work, otherwise, create your own
+    #group_to_run = message["group"]
+    #groupkeys = list(group_to_run.keys())
+    #groupkeys.sort()
+    #metadataID = '-'.join(groupkeys)
+    
+    # Add a handler with 
+    # watchtowerlogger=watchtower.CloudWatchLogHandler(log_group=LOG_GROUP_NAME, stream_name=str(metadataID),create_log_group=False)
+    # logger.addHandler(watchtowerlogger)
+
+    # See if this is a message you've already handled, if you've so chosen    
+    # First, build a variable called remoteOut that equals your unique prefix of where your output should be 
     # Then check if there are too many files
-    if CHECK_IF_DONE_BOOL.upper() == "TRUE":
+    
+    if CHECK_IF_DONE_BOOL.upper() == 'TRUE':
         try:
-            s3client = boto3.client("s3")
-            bucketlist = s3client.list_objects(
-                Bucket=AWS_BUCKET, Prefix=remoteOut + "/"
-            )
-            objectsizelist = [k["Size"] for k in bucketlist["Contents"]]
+            s3client=boto3.client('s3')
+            bucketlist=s3client.list_objects(Bucket=AWS_BUCKET,Prefix=remoteOut+'/')
+            objectsizelist=[k['Size'] for k in bucketlist['Contents']]
             objectsizelist = [i for i in objectsizelist if i >= MIN_FILE_SIZE_BYTES]
             if NECESSARY_STRING:
-                if NECESSARY_STRING != "":
-                    objectsizelist = [
-                        i for i in objectsizelist if NECESSARY_STRING in i
-                    ]
-            if len(objectsizelist) >= int(EXPECTED_NUMBER_FILES):
-                printandlog("File not run due to > expected number of files", logger)
+                if NECESSARY_STRING != '':
+                    objectsizelist = [i for i in objectsizelist if NECESSARY_STRING in i]
+            if len(objectsizelist)>=int(EXPECTED_NUMBER_FILES):
+                printandlog('File not run due to > expected number of files',logger)
                 logger.removeHandler(watchtowerlogger)
-                return "SUCCESS"
-        except KeyError:  # Returned if that folder does not exist
-            pass
-
-    # If you need to download files locally, perform that step here
-    # printandlog("Downloading files", logger)
+                return 'SUCCESS'
+        except KeyError: #Returned if that folder does not exist
+            pass	
 
     # Build and run your program's command
-    # e.g. cmd = my-program --my-flag-1 True --my-flag-2 VARIABLE
+    # ie cmd = my-program --my-flag-1 True --my-flag-2 VARIABLE
+    # you should assign the variable "localOut" to the output location where you expect your program to put files
 
-    # Assign the variable "localOut" to the output location where you expect your program to put files
-    # e.g. localOut = os.path.join(LOCAL_OUTPUT, metadataID)
-
-    print("Running", cmd)
+    print('Running', cmd)
     logger.info(cmd)
-    subp = subprocess.Popen(
-        cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT
-    )
-    monitorAndLog(subp, logger)
+    subp = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+    monitorAndLog(subp,logger)
 
-    # Figure out a done condition - a number of files being created, a particular file/folder being created, an exit code, etc.
+    # Figure out a done condition - a number of files being created, a particular file being created, an exit code, etc. 
     # Set its success to the boolean variable `done`
-    # e.g. done = os.path.isfile(os.path.join(localOut, program.is.done))
-
+    
     # Get the outputs and move them to S3
     if done:
         time.sleep(30)
-        mvtries = 0
-        while mvtries < 3:
+        mvtries=0
+        while mvtries <3:
             try:
-                printandlog("Move attempt #" + str(mvtries + 1), logger)
-                cmd = (
-                    "aws s3 mv "
-                    + localOut
-                    + " s3://"
-                    + AWS_BUCKET
-                    + "/"
-                    + remoteOut
-                    + " --recursive"
-                )
-                subp = subprocess.Popen(
-                    cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE
-                )
-                out, err = subp.communicate()
-                out = out.decode()
-                err = err.decode()
-                printandlog("== OUT \n" + out, logger)
-                if err == "":
-                    break
-                else:
-                    printandlog("== ERR \n" + err, logger)
-                    mvtries += 1
+                    printandlog('Move attempt #'+str(mvtries+1),logger)
+                    cmd = 'aws s3 mv ' + localOut + ' s3://' + AWS_BUCKET + '/' + remoteOut + ' --recursive' 
+                    subp = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
+                    out,err = subp.communicate()
+                    out=out.decode()
+                    err=err.decode()
+                    printandlog('== OUT \n'+out, logger)
+                    if err == '':
+                        break
+                    else:
+                        printandlog('== ERR \n'+err,logger)
+                        mvtries+=1
             except:
-                printandlog("Move failed", logger)
-                printandlog("== ERR \n" + err, logger)
+                printandlog('Move failed',logger)
+                printandlog('== ERR \n'+err,logger)
                 time.sleep(30)
-                mvtries += 1
+                mvtries+=1
         if mvtries < 3:
-            printandlog("SUCCESS", logger)
+            printandlog('SUCCESS',logger)
             logger.removeHandler(watchtowerlogger)
-            return "SUCCESS"
+            return 'SUCCESS'
         else:
-            printandlog(
-                "SYNC PROBLEM. Giving up on trying to sync " + metadataID, logger
-            )
+            printandlog('SYNC PROBLEM. Giving up on trying to sync '+metadataID,logger)
             import shutil
-
             shutil.rmtree(localOut, ignore_errors=True)
             logger.removeHandler(watchtowerlogger)
-            return "PROBLEM"
+            return 'PROBLEM'
     else:
-        printandlog("PROBLEM: Failed exit condition for " + metadataID, logger)
+        printandlog('PROBLEM: Failed exit condition for '+metadataID,logger)
         logger.removeHandler(watchtowerlogger)
         import shutil
-
         shutil.rmtree(localOut, ignore_errors=True)
-        return "PROBLEM"
-
+        return 'PROBLEM'
+    
 
 #################################
 # MAIN WORKER LOOP
 #################################
 
-
 def main():
     queue = JobQueue(QUEUE_URL)
     # Main loop. Keep reading messages while they are available in SQS
@@ -233,23 +197,23 @@ def main():
         msg, handle = queue.readMessage()
         if msg is not None:
             result = runSomething(msg)
-            if result == "SUCCESS":
-                print("Batch completed successfully.")
+            if result == 'SUCCESS':
+                print('Batch completed successfully.')
                 queue.deleteMessage(handle)
             else:
-                print("Returning message to the queue.")
+                print('Returning message to the queue.')
                 queue.returnMessage(handle)
         else:
-            print("No messages in the queue")
+            print('No messages in the queue')
             break
 
-
 #################################
 # MODULE ENTRY POINT
 #################################
 
-if __name__ == "__main__":
+if __name__ == '__main__':
     logging.basicConfig(level=logging.INFO)
-    print("Worker started")
+    print('Worker started')
     main()
-    print("Worker finished")
+    print('Worker finished')
+
diff --git a/worker/instance-monitor.py b/worker/instance-monitor.py
index 8ffca40..dec511a 100644
--- a/worker/instance-monitor.py
+++ b/worker/instance-monitor.py
@@ -9,18 +9,16 @@
 import time
 import logging
 
-
 def monitor():
     logger = logging.getLogger(__name__)
     while True:
-        cmdlist = ["df -h", "df -i -h", "vmstat -a -SM 1 1", "iostat"]
+        cmdlist=['df -h', 'df -i -h','vmstat -a -SM 1 1', 'iostat']
         for cmd in cmdlist:
-            process = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
-            out, err = process.communicate()
+            process=subprocess.Popen(cmd.split(),stdout=subprocess.PIPE)
+            out,err=process.communicate()
             logger.info(out)
         time.sleep(30)
-
-
-if __name__ == "__main__":
+        
+if __name__=='__main__':
     logging.basicConfig(level=logging.INFO)
     monitor()
diff --git a/worker/run-worker.sh b/worker/run-worker.sh
index 6fbf701..fb64ad8 100644
--- a/worker/run-worker.sh
+++ b/worker/run-worker.sh
@@ -17,16 +17,15 @@ aws ec2 create-tags --resources $VOL_0_ID --tags Key=Name,Value=${APP_NAME}Worke
 VOL_1_ID=$(aws ec2 describe-instance-attribute --instance-id $MY_INSTANCE_ID --attribute blockDeviceMapping --output text --query BlockDeviceMappings[1].Ebs.[VolumeId])
 aws ec2 create-tags --resources $VOL_1_ID --tags Key=Name,Value=${APP_NAME}Worker
 
-# 2. MOUNT S3
-# Remove this if not mounting S3 bucket
+# 2. MOUNT S3 
 echo $AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY > /credentials.txt
 chmod 600 /credentials.txt
 mkdir -p /home/ubuntu/bucket
 mkdir -p /home/ubuntu/local_output
-stdbuf -o0 s3fs $AWS_BUCKET /home/ubuntu/bucket -o passwd_file=/credentials.txt
+stdbuf -o0 s3fs $AWS_BUCKET /home/ubuntu/bucket -o passwd_file=/credentials.txt 
 
 # 3. SET UP ALARMS
-aws cloudwatch put-metric-alarm --alarm-name ${APP_NAME}_${MY_INSTANCE_ID} --alarm-actions arn:aws:swf:${AWS_REGION}:${OWNER_ID}:action/actions/AWS_EC2.InstanceId.Terminate/1.0 --statistic Maximum --period 60 --threshold 1 --comparison-operator LessThanThreshold --metric-name CPUUtilization --namespace AWS/EC2 --evaluation-periods 15 --dimensions "Name=InstanceId,Value=${MY_INSTANCE_ID}"
+aws cloudwatch put-metric-alarm --alarm-name ${APP_NAME}_${MY_INSTANCE_ID} --alarm-actions arn:aws:swf:${AWS_REGION}:${OWNER_ID}:action/actions/AWS_EC2.InstanceId.Terminate/1.0 --statistic Maximum --period 60 --threshold 1 --comparison-operator LessThanThreshold --metric-name CPUUtilization --namespace AWS/EC2 --evaluation-periods 15 --dimensions "Name=InstanceId,Value=${MY_INSTANCE_ID}" 
 
 # 4. RUN VM STAT MONITOR
 
@@ -38,3 +37,4 @@ for((k=0; k<$DOCKER_CORES; k++)); do
     sleep $SECONDS_TO_START
 done
 wait
+