Lorikeet 0.11.0 - Upgrading to async

Upgrading lorikeet to async rust
rust lorikeet 2020-04-21

I have just spent some time doing an initial async version of lorikeet now that the async/await syntax is stable and the ecosystem has caught up. The major blocker was reqwest, as this is used extensively in the http test.

This async version is available now as version 0.11.0. You can also install the cli by running cargo install lorikeet.

What is Lorikeet

Lorikeet is a command line tool and a rust library to run tests for smoke testing and integration testing. Lorikeet currently supports bash commands and simple HTTP requests along with system information (RAM, Disk & CPU). There is more information on the github readme about how to write test files, including how to structure dependent tests and make assertions on the output.

As a simple example, here's a test plan to check to see whether reddit is up, and then tries to login if it is:

check_reddit:
  http: https://www.reddit.com
  regex: the front page of the internet

login_to_reddit:
  http: 
    url: https://www.reddit.com/api/login/{{user}}
    form:
      user: {{user}}
      passwd: {{pass}}
      api_type: json
  jmespath: length(json.errors)
  matches: 0
  require:
    - check_reddit

You can run this as a simple cli command, ensuring that you have a config.yml looks something like:

user: test
pass: test

Running it you will see the results:

$ lorikeet -c config.yml test.yml
- name: check_reddit
  pass: true
  output: the front page of the internet
  duration: 1416.591ms

- name: login_to_reddit
  pass: true
  output: 0
  duration: 1089.0276ms

Skipping futures 0.1

In retrospect, I am glad that I skipped out on rewriting lorikeet with the classic combinator style futures. The migration from standard blocking code to async code is much easier than writing from combinators. Having done the former with mpart-async, it felt quite easy to sprinkle in a few async and await statements to get things all wired up.

As an example, waiting for the request used to be:

let mut response = client
    .execute(request.build().map_err(|err| format!("{:?}", err))?)
    .map_err(|err| format!("Error connecting to url {}", err))?;

Now, with async, things are not much different:

let response = client
    .execute(request.build().map_err(|err| format!("{:?}", err))?)
    .await
    .map_err(|err| format!("Error connecting to url {}", err))?;

Using tokio sync primitives

Tokio has a few primitives that are very close to their std counterparts.

Lorikeet now uses a tokio mutex to record the outcome of each of the steps:

steps.lock().await[index] = Status::Completed(outcome);

Also using an unbounded channel so each step can inform the main runner when they are finished:

let (tx, mut rx) = unbounded_channel();

// later on....

if let Some(finished_idx) = rx.recv().await {
  ...
}

Interesting to note that the UnboundedSender send() method is not async and does not block either.

Multipart File Uploads

The new version of reqwest does not support file uploads easily. You don't want to buffer the entire file in memory. Luckily I have had some experience on how to wire this up with mpart-async, and can reuse some of the learnings there.

This does require the tokio-util crate, which bridges AsyncRead/AsyncWrite with Sink/Stream.

The relevent glue section is as follows:

let file_name = path_struct
    .file
    .file_name()
    .map(|val| val.to_string_lossy().to_string())
    .unwrap_or_default();

let file = File::open(&path_struct.file)
    .await
    .map_err(|err| format!("{:?}", err))?;

let reader = Body::wrap_stream(FramedRead::new(file, BytesCodec::new()));

form.part(key, Part::stream(reader).file_name(file_name)

Detecting Blocking Code

There is no annotation or warning whenever you are using blocking code in an async context. This is an issue especially when porting over older code which was previously blocking. Chances are I have missed a few sections that are blocking and will need to revisit them accordingly.

Luckily, the fail case for blocking code is usually reduced throughput, down to the number of worker threads, so it is not cataclysmic if there are some blocking sections in your async code.

Conclusion

I was happy with the ease at which it was to convert existing sections to async by sprinkling a few keywords here and there. I am concerned that it is too easy to have blocking sections, so I hope there is some tooling/linting around that in the future.

Please give lorikeet a go and try out some of the newer features from the readme!