hatoo / oha

Ohayou(おはよう), HTTP load generator, inspired by rakyll/hey with tui animation.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Adding delay between requests

liketoeatcheese opened this issue · comments

Hi,

Is there a way to add a delay between requests? Like 200 requests for one worker thread, wait for 5s between each request?

Sorry, you can do similar but there are limitations.

You can put -q option to set query per second e.g. if you set -q 1, oha will do 1 request per second.
But it's not a delay, the actual delay would be shorter than 1 second.
And -q option only accepts an integer, you cannot specify less than 1 query per second.

Thanks for clarifying :)

Hey hatoo,

Based on your comment, I looked into the code and want to see if I can contribute anything, as I use this quite often. One thing I noticed, and maybe a question on why it's being set up that way:

Looking at -q, which is qps, the function for processing that option is Client::work_until_with_qps. Hypothetically, assuming there are 7 qps, and 5 workers. In that function, you create a queue with the same size as qps, and in the sender, you do a tokio sleep of an amount of time relative to qps and send it to the queue. And in the receiver end, 5 workers will constantly waiting to consume the data. However, isn't query per second means every second, 7 requests are sent for processing? While this looks like one request is sent at a time with some time delayed between it, and all available workers consume it right away.

And in terms of delay, I assume that every work*() related function should accept a delay: Option<human::Duration>, and be processed by each worker before sending to the queue, and within the validation process with n_tasks or deadline with something like this?:

match delay {
    Some(duration) => {
        tokio::time::sleep(duration.into()).await;
        return ();
    }
    None => (),
};

This way all workers will respect the delay. Let me know what you think :)

While this looks like one request is sent at a time with some time delayed between it, and all available workers consume it right away.

Sorry, I don't get that. I think this explanation seems right and it means we are sending 7 queries per second.

And in terms of delay, I assume that every work*() related function should accept a delay: Optionhuman::Duration, and be processed by each worker before sending to the queue, and within the validation process with n_tasks or deadline with something like this?:

Looks good, but I have some opinions

  • human::Duration only be used in command line parsing. std::time::Duration should be sent to work*() functions
  • delay should be processed in work*() functions because I want the client module to keep slim.

But at first, I want to know why you want to introduce delay rather than just setting query per second :)

Reason why I want to introduce delay

I'm a data engineer at work, and often we do need to test some memory leak issues where we run a certain amount of loads but spread out, so for example, 200 requests, 10 requests per second (correct me if I'm wrong but I use 10 workers for this, as I assume 10 workers represent 10 different clients hitting the API at once which portrait better than qps as qps is more of a rate-limiting functionality - e.g: 10 qps means you can only do 10 querys per second max, no matter how many workers you have) and with a delay of 10 seconds. Meaning every 10 seconds, send 10 requests until it reaches 200 requests. And I'd normally use SOAPUI for this as it's a feature, but I prefer to use oha as it's normally my go to for load testing now.

QPS questionnaire

For some reason, probably it's because it was towards the end of the day, I misread the bounded queue (channel) of 7, your code is unbounded (flume::unbounded()). But my question is still the same, so the code which I'm reading in client::work_with_qps:

    tokio::spawn(async move {
        let start = std::time::Instant::now();
        for i in 0..n_tasks {
            tokio::time::sleep_until(
                (start + i as u32 * std::time::Duration::from_secs(1) / qps as u32).into(),
            )
            .await;
            tx.send_async(()).await.unwrap();
        }
        // tx gone
    });

From my understanding, a green thread is spawned, and you loop through the number of tasks, tokio::time::sleep_until will sleep to a point in time of: (start + i as u32 * std::time::Duration::from_secs(1) / qps as u32).into(), send to the queue (channel), and then go to the next task. Doesn't that mean you send one task every few seconds instead of sending a few tasks every one second? My thoughts would be you have a number of sender workers (e.g 7 qps would be 7 sender workers), send to the channel at once, wait till the next second, and send it again until you have finished all n_tasks. And n_workers` will be on the receiver end, so no matter how many workers there are, you will only send 7 requests per second, and 7 requests will be consumed.
Yours:
image

What I think:
image

Reply on your opinions

  • human::Duration only be used in command line parsing. std::time::Duration should be sent to work*() functions

human::Duration do implement From<std::time::Duration>, so we can always use .into() to convert. So that's fine

  • delay should be processed in work*() functions because I want the client module to keep slim.

So do you mean you want to refactor work*() functions to a separate mod? As these functions are currently in client mod.

Let us discuss about qps first.

Doesn't that mean you send one task every few seconds instead of sending a few tasks every one second?

Oha

I'm guessing you want the qps implementation below on the image.
But why? I think the current implementation seems more natural.

Ohhhhh, you spread one second to the number of smaller microseconds based on qps. Ok, that makes sense. That's really smart! I finally understand the (start + i as u32 * std::time::Duration::from_secs(1) / qps as u32).into() part

For that, yes you're right

So do you want the delay feature still?
I think you can use qps instead.

Yes. I still do. qps limits at 1s. So no matter what value is set for qps, every second, there will be at least 1 request were fired. But I'd like to have a delay of more than 1s between each request. Something like this (Assuming a delay of 7s were provided:
image

Sounds good.

Here is my rough idea.

  1. Add a command line argument, somewhat --burst [delay] [requests]
  • it's not compatible with -q
  1. Extend work*qps* functions to receive an enum like the below instead of just qps integer
enum QueryLimit {
    Qps(usize),
    Burst(Duration, usize)
}
  1. Update those function's sender part

I'm happy if you do or I'll do it this weekend.

I will do it and make a pull request when I'm done. I will update the comments if I have any questions

Hi @hatoo, I finished the code. I wrote it in a separate branch, how do I push this to remote and make a pull request?

@liketoeatcheese

Roughly, you should follow these steps.
Please add comments if any steps of these are unclear.

  1. Fork this repo in GitHub
  2. Add your ssh key to your GitHub account
  3. Run git remote set-url origin ${YOUR_FORKED_REPO_URL} in local
  4. Push the branch to your forked repo
  5. Making pull request button should be appeared on GitHub

I created a pull request just now. Let me know if you have any questions or input