ob-ivan / throttler

Execute tasks in a loop at limited rate
MIT License
0 stars 0 forks source link

Implementation explanation request #2

Closed kumarmanishc closed 3 years ago

kumarmanishc commented 3 years ago

I have one class and function is responsible for making api call. and ExecuteCurlCallGet is being called from various places in my project. And I want to limit the execution of this function to execute for only 100 api calls.

class ExecutecallClass {
    public function __construct(){}

        //Make api calls through function...
    function ExecuteCurlCallGet($url) { 

              $curl = curl_init();
              curl_setopt_array($curl, array(
              CURLOPT_URL => $url,
              CURLOPT_RETURNTRANSFER => true,
              CURLOPT_CUSTOMREQUEST => 'GET',
              CURLOPT_HTTPHEADER => array(
                "Authorization: Bearer ".$__token
              ),
            ));
            $response = curl_exec($curl);
            curl_close($curl);
            $json = json_decode($response);
            return $json;
    }
}

In documentation it states that $throttler->run($job);

When I implement class will have next and execute method and we can create instance as follows, $job = new ExecutecallClass();

But my question is how can I call my function to other places in my project.

If I am miss understanding something please let me know.

Thank You

ob-ivan commented 3 years ago

Hi @kumarmanishc! Thanks for the interest in this tiny project.

Let me clarify the question.

Do I guess correct so far?

If I guessed correct, what you need is to implement Ob_Ivan\Throttler\JobInterface with a class that takes the first portion of your list as a constructor argument (use array_slice() on your long list of URLs before passing it to the constructor). The next() method would shift the list and tell if the list became empty. This is needed to make the throttler stop once all calls have been made. Lastly, the execute() method would call ExecutecallClass::ExecuteCurlCallGet() with the URL that is the first in the list.

I would say that what you want is to wrap ExecutecallClass with a job rather that use ExecutecallClass as a job.

I hope this gives a hint about the supposed usage of the Throttler class.

Please don't hesitate to ask further questions or correct any of the assumptions I relied on.

kumarmanishc commented 3 years ago

@ob-ivan Thank You for your reply. I want to mention to this question,

  1. There is one base API URL and many endpoints that can be called from any function independently using ExecuteCurlCallGet function.
  2. We need to limit number of API calls to 100 API calls per minute to that base API URL.
ob-ivan commented 3 years ago

Aha, I see now.

Please note that an Ob_Ivan\Throttler\Throttler object carries its state over after executing a job.

This means that if you supply it with single-URL jobs, it will sleep before executing the 101st API call.

So what you need is to:

As long as the same throttler object is injected everywhere, it will limit the API call rate.

Your SingleJob class can be outlined like following:

class SingleJob implements Ob_Ivan\Throttler\JobInterface {
    private $url;
    private $result = null;
    public function __construct($url) { $this->url = $url; }
    public function next() { return $this->result === null; }
    public function execute() {
        $executeCall = new ExecutecallClass();
        $this->result = $executeCall->ExecuteCurlCallGet($this->url);
    }
    public function getResult() { return $this->result; }
}

Please note that due to the known bug #1, the current implementation may occasionally sleep even when no sleep is needed.

I also recommend hiding a throttler object behind an adapter class to avoid vendor lock.

Hope this helps.

ob-ivan commented 3 years ago

Of course this will not work well for you:

$throttler->execute(new SingleJob($url))

because you won't be able to retrieve the results then!

What you need is to create a job object, pass it to execute(), then retrieve the results, like following:

$job = new SingleJob($url);
$this->throttler->execute($job);
$result = $job->getResult();

But then again, if you hide it behind your own adapter, this might look like a single call:

interface ThrottledApiCall {
    public function call($url);
}
class ObIvanThrottledApiCall implements ThrottledApiCall {
    private $throttler;
    public function __construct() { $this->throttler = new Ob_Ivan\Throttler\Throttler(100, 60); }
    public function call($url) {
        $job = new SingleJob($url);
        $this->throttler->execute($job);
        return $job->getResult();
    }
}

This way if you want to adopt an alternative throttling library, you just create a new implementation of ThrottledApiCall and you don't need to change the way you use it in the rest of your code:

function doSomethingUseful(ThrottledApiCall $apiCall) {
    // do something else, maybe find out the URL
    // ...
    $result = $apiCall->call($url);
    // process the results
    // ...
}
kumarmanishc commented 3 years ago

Thank You @ob-ivan for helping me out. I will try to your suggestion if we can get it working then will update you.

kumarmanishc commented 3 years ago

This is code how we are making api calls. This is snippets I am adding so you can understand my scenario,

// Function can be called in loop.
 sync_groupitem_recursively($page, $category, $source);

//Function to make api call
function sync_groupitem_recursively($page, $source)
{
    $url                = $zoho_inventory_url . 'api/v1/pricebooks?organization_id=' . $zoho_inventory_oid;
        // This is making api calls
    $executeCurlCallHandle = new ExecutecallClass();
    $json = $executeCurlCallHandle->ExecuteCurlCallGet($url);
    $json2 = json_encode($json);
    return json_decode($json2, true);
}

Function of ExecuteCallClass, but there are other functions as well,

class ExecuteCallClass { 
function ExecuteCurlCallImageGet($url, $image_name) {   

            $handlefunction = new Classfunctions;

            $zoho_inventory_access_token = $this->config['ExecutecallZI']['ATOKEN'];
            $zoho_inventory_refresh_token = $this->config['ExecutecallZI']['RTOKEN'];
            $zoho_inventory_timestamp = $this->config['ExecutecallZI']['EXPIRESTIME'];

            $current_time = strtotime(date('Y-m-d H:i:s'));

            if($zoho_inventory_timestamp < $current_time){

                $respoAtJs = $handlefunction->GetServiceZIRefreshToken($zoho_inventory_refresh_token);

                $zoho_inventory_access_token = $respoAtJs['access_token'];
                update_option('zoho_inventory_access_token', $respoAtJs['access_token']);
                update_option('zoho_inventory_timestamp', strtotime(date('Y-m-d H:i:s'))+$respoAtJs['expires_in']);

            }

            $curl = curl_init();
            curl_setopt_array($curl, array(
              CURLOPT_URL => $url,
              CURLOPT_RETURNTRANSFER => true,
              CURLOPT_ENCODING => "",
              CURLOPT_MAXREDIRS => 10,
              CURLOPT_TIMEOUT => 0,
              CURLOPT_FOLLOWLOCATION => true,
              CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
              CURLOPT_CUSTOMREQUEST => "GET",
              CURLOPT_HTTPHEADER => array(
                "Authorization: Bearer ".$zoho_inventory_access_token,
              ),
            ));
            $response = curl_exec($curl);
            curl_close($curl);

            $upload = wp_upload_dir();
            $absolute_upload_path = $upload['basedir'].'/zoho_image';
            $url_upload_path = $upload['baseurl'].'/zoho_image';

            $img = '/'.rand().$image_name;
            //$img = '/image'.rand().'.jpg';
            $upload_dir = $absolute_upload_path.$img;

            if(!is_dir($absolute_upload_path)){
              mkdir($absolute_upload_path);
            }

            file_put_contents($upload_dir, $response);

        return $url_upload_path.$img;

    }
} // Closing of class.
ob-ivan commented 3 years ago

@kumarmanishc, I'm not sure I understand whether you still have a question, and if you do, what the question is.

What I understand is that you have a class ExecuteCallClass, and it features many methods, some of them including a cURL request written out verbosely, probably due to the high variance of their options and the purposes the response is used for.

What I wanted to point out is that the throttling will not work unless you have a throttler object shared by all calls to the same API. I guess you can use some WordPress magic to have a throttler singleton or a globally shared variable, whatever you like, but I'm not much of an expert in WordPress's best practices, so you will have to figure that out.

If the URL is not the only input parameter of an API call, you could implement the SingleJob class as a wrapper for a $curl resource, which execute()s by calling $this->response = curl_exec($curl), then you expose the response via a getter.

If you still have a question, please ask a question.

kumarmanishc commented 3 years ago

@ob-ivan You said throttling will not work unless you have a throttler object shared by all calls to the same API What I understand is creating instance of ObIvanThrottledApiCall and share among all api calls we are making ? as follows,

$apiCall = new ObIvanThrottledApiCall();
doSomethingUseful($apiCall);
function doSomethingUseful(ThrottledApiCall $apiCall) {
    // do something else, maybe find out the URL
    // ...
    $result = $apiCall->call($url);
    // process the results
    // ...
}

Please let me know if I'm wrong. Also if not that scenario, Can you please work with us for completion of this task ?

ob-ivan commented 3 years ago

@kumarmanishc You said in the OP:

ExecuteCurlCallGet is being called from various places in my project.

I don't know how your project is structured, but let me assume something.

Let's say you have ClassA and ClassB. Each has its own method to make an API call:

class ClassA {
    public function makeApiCall($parameter1) {
        $curl = curl_init();
        ... set options ...
        $result = curl_exec($curl);
        curl_close($curl);
        return $result;
    }
}

class ClassB {
    public function makeApiCall($parameter1) {
        $curl = curl_init();
        etc...
    }
}

What I was trying to say is that if you instantiate a throttler each time you need to make an API call like this:

class ClassA {
    public function makeApiCall(...) {
        $throttler = new Throttler();
        ...
    }
}

class ClassB {
    public function makeApiCall(...) {
        $throttler = new Throttler();
        ...
    }
}

then each throttler will track its request rate separately. And if you let a throttler object be destroyed (by leaving the function scope), then its information about how many requests has been executed in what time frame will be lost, meaning that the subsequent requests will not be limited to the target request rate and the API will reject your requests.

So if you have such classes, you need to make sure that each class receives the very same object of the throttler, for example, like this:

class ClassWhereAllThingsHappen {
    public function functionThatMakesEveryoneHappy() {
        $throttler = new Throttler(100, 60); // This will be the object that traces the request rate.
        $classA = new ClassA($throttler); // Make ClassA's requests be traced.
        $classB = new ClassB($throttler); // Make ClassB's requests be traced.
        ... do fun stuff with $classA and $class B ...
    }
}

Where will the throttler object be initialized in your project, how will the throttler object reach its consumers --- this all depends on how your project is structured, and this is something you will have to figure out by yourself.

kumarmanishc commented 3 years ago

@ob-ivan If you are available for paid task for this issue please shoot one mail at info@roadmapstudios.com

ob-ivan commented 3 years ago

@kumarmanishc Thank you for the offer, but I am not.

ob-ivan commented 3 years ago

Thanks for the interest in this little project. The original question was discussed at length already. If the information provided is insufficient, please try to narrow down your question and specify a concrete problem you would like to address.