Developer Journal: 29 April 2020

Posted on Wed 29 April 2020 in dev-journal

Updating the dev-journal script to open previous day

This morning when starting this entry I wanted to be able to check look at what I was was working on yesterday and found that what I've been doing is navigating to the file and opening the previous day. However, since I wrote a script that will open a journal entry, I can add an argument that will allow me to open a previous day. This was done by changing the way I was using the date function. Previously the line of code that I used to get the date looked like this:

DATE=$(date +20%y-%m-%d)

That worked well but then I wanted to be able to increment or decrement the day. It took me a few tries but it did allow me to learn a little bit more about the bash date function.

# use a command line argument to change the date
if $1; then
    DATE=$(date -d "+$1 days" +20%y-%m-%d)
else
    DATE=$(date +20%y-%m-%d)
fi

So this was the first iteration that I used.. it worked great for getting previous entries however it was adding a day if you called dev-journal without any arguments. To fix this the script was again modified, the final version of this date logic looks as follows:

if [ -z "$1" ]; then
    DATE=$(date +20%y-%m-%d)
else
    DATE=$(date -d "+$1 days" +20%y-%m-%d)
fi

This uses the -z to test if $1 is an empty string. If it is than we set the date to now otherwise, apply the command line argument. This currently does not handle any validation with user input. That can be added if the script needs to be expanded further in the future.

Writing this also allowed me to recall a git command that I rarely use, the git clean command. This command can be very useful if you have created a lot of files in your git repo that have no reason to be there. In this case while I was working to git this command working the script created some files that did not match what I was looking for.

michaelabrahamsen.com git:(master) ✗ git clean -n
Would remove content/posts/dev-journal/-developer-journal.md
Would remove content/posts/dev-journal/2020-04-29+-1-developer-journal.md
Would remove content/posts/dev-journal/2020-04-29-developer-journal.md
Would remove content/posts/dev-journal/2020-04-30-developer-journal.md
Would remove content/posts/dev-journal/Sat 09 May 2020 09:17:46 AM MDT-developer-journal.md
Would remove content/posts/dev-journal/Tue 28 Apr 2020 09:21:37 AM MDT-developer-journal.md

git clean -n performs a dry run so you can see what will be deleted and git clean -f will remove the files that the previous command listed.

Shutting down a virtualbox vm via command line

VBoxManage controlvm VM-NAME poweroff

Using custom RQ workers with Conveyor.dev

In order to change Conveyor's current worker creation to allow for custom RQ workers that was shown previously Conveyor will need an extra form field. The form field will determine what command is run when configuring and starting the workers.

So where to start... yesterday I added a form field for the custom command. Let's write a couple of tests and add the field to the model.

def test_model_has_custom_worker_field(self):
    worker = SiteWorker(1, 1, queue_names="high default", worker_count=1)
    self.assertEqual(worker.worker_command, "rq worker")

def test_model_sets_custom_worker_field(self):
    worker = SiteWorker(1, 1, queue_names="high default", worker_count=1,
                        worker_command="rq-gevent")
    self.assertEqual(worker.worker_command, "rq-gevent")

Now to make these pass we just need to update the model:

class SiteWorker(db.Model):
    __tablename__ = 'site_worker'
    id = db.Column(db.Integer, primary_key=True)
    worker_count = db.Column(db.Integer)
    queue_names = db.Column(db.String)
    worker_command = db.Column(db.String)

    def __init__(self, worker_count, queue_names, worker_command=None):
        self.worker_count = worker_count
        self.queue_names = queue_names
        self.worker_command = worker_command or "rq worker"

To add the form field to the view:

{{ render_field(form.custom_worker, placeholder="rq-gevent-worker", class="w-full input") }}

So all of that was easy... now to make sure that the correct files get created on the server. More tests! This time we want to make sure the worker manager and the individual worker processes are created correctly. In this case it just means that we pass along the command and make sure that is added to the config script.

def test_formatting_with_single_queue_and_custom_worker(self):
    domain = "test.test.com"
    queues = "high"
    manager = "test.test.com-worker-1.service"
    expected_output = (
        "ExecStart=/bin/bash -c 'source "
        f"/home/conveyor/{domain}/venv/bin/activate && rq-gevent {queues}'"
    )
    config = Client.get_provision_file_contents(
        "[email protected]")
    formatted_config = Provisioner.format_provision_config(
        config, domain=domain, queues=queues, worker_manager=manager,
        command="rq-gevent")
    output_line = self.get_line_containing(formatted_config, "Exec")
    self.assertEqual(output_line, expected_output)

def test_formatting_with_multiple_queues_and_custom_worker(self):
    domain = "test.test.com"
    queues = "high default"
    manager = "test.test.com-worker-1.service"
    expected_output = (
        "ExecStart=/bin/bash -c 'source "
        f"/home/conveyor/{domain}/venv/bin/activate && rq-gevent {queues}'"
    )
    config = Client.get_provision_file_contents(
        "[email protected]")
    formatted_config = Provisioner.format_provision_config(
        config, domain=domain, queues=queues, worker_manager=manager,
        command="rq-gevent")
    output_line = self.get_line_containing(formatted_config, "Exec")
    self.assertEqual(output_line, expected_output)

This actually leads to a piece of code that is so simple yet so powerful. Part of the client has a class method to format config files. Some of these configs need to have arguments passed in. I store a base config file as a string with some string parameterization, then the following function formats the file as necessary:

@classmethod
def format_provision_config(cls, contents, **kwargs):
    return contents.format(**kwargs)

After doing some manual testing, the worker services were not being started/stopped correctly. They did not get the memo that we were adding the worker_id to the filename. The commands that are doing that are now used in multiple places so they were broken out into their own functions:

@classmethod
def get_worker_service(cls, domain, worker_id):
    return f"{domain}-worker-{worker_id}.service"

@classmethod
def get_worker_file_name(cls, domain, worker_id):
    return f"/etc/systemd/system/{domain}-worker-{worker_id}@.service"

@classmethod
def get_worker_manager_file_name(cls, domain, worker_id):
    return f"/etc/systemd/system/{domain}-worker-{worker_id}.service"

Now for the real test and the purpose of all of this. Can we deploy this using Conveyor.dev and use our custom worker class?

Resources

Hero Icons