Skip to content

Instantly share code, notes, and snippets.

@gal-bert
Last active August 4, 2022 04:00
Show Gist options
  • Save gal-bert/474cadda5664bcbc41622fd84c9c252d to your computer and use it in GitHub Desktop.
Save gal-bert/474cadda5664bcbc41622fd84c9c252d to your computer and use it in GitHub Desktop.
Dealing with Race Conditions In Chained API Calls

Dealing with Race Conditions In Chained API Calls

Hey there coders!

Thanks for stopping by this article. In this article, I’m going to share a bit about my experience while handling chained API Calls in my Nano Challenge 2 application Colorio. Here I will share my stupid ghetto way in handling them, and share other alternative that can be used.


Keywords:

  • Race Condition
    • Completion Handlers
    • Recursion
    • Semaphores

Let’s start with a basic API call

Have you ever wanted to use an API and load their data to a tableview? Well usually you will do this in your view controller:

Example JSON data fetched from API:

[
  {
    "id": 1,
    "author": "Gregorius Albert",
    "content": "This is my first tweet"
  },
  {
    "id": 2,
    "author": "Taylor Swift",
    "content": "It's August babyyyyy"
  },
  {
    "id": 3,
    "author": "Justin Bieber",
    "content": "What's about the Baby song thing?"
  }
]

Code to fetch data from API and load to tableview:

let url = URL(string: Helper.BASE_URL)!
var request = URLRequest(url: url)
request.httpMethod = "GET"

URLSession.shared.dataTask(with: request){ (data, response, error) in

    let json = try! JSONSerialization.jsonObject(with: data!) as! [[String:Any]]
	
    for result in json {
        let author = result["author"] as! String
        let content = result["content"] as! String
        let tweet = Tweet(author: author, content: content)
        self.tweets.append(tweet)
    }
	
    DispatchQueue.main.async {
        self.tableView.reloadData()
    }
	
}.resume()

This solution works if the API can return a data in bulk or collection. It’s like doing a SELECT * in an SQL Database.


Using a public API that doesn’t provide bulk data

But what if you are using a public API that doesn’t provide bulk data? Some API only allows you to get a single data based on a parameter. Take a look below for an example.

API URL: https://www.thecolorapi.com/id?hex=E62028

Returned result:

{
  "hex": {
    "value": "#E62028",
    "clean": "E62028"
  },
  "rgb": {
    "fraction": {
      "r": 0.9019607843137255,
      "g": 0.12549019607843137,
      "b": 0.1568627450980392
    },
    "r": 230,
    "g": 32,
    "b": 40,
    "value": "rgb(230, 32, 40)"
  },
  "name": {
    "value": "Alizarin Crimson",
    "closest_named_hex": "#E32636",
    "exact_match_name": false,
    "distance": 349
  }
}

// Some value from the API have been deleted to shorten this article

Here I get the value from a parameter that I gave in the URL which is hex=E62028. But I need to get the data for multiple colors at once. How can I do that?

What I tried

Well, everyone might think “just loop the API call”. Well you’re technically can loop an API call. Let’s try to loop 5 hex codes and get the color name from each hex code in the array.

let hexArr = ["FFFFFF", "000000", "FF0000", "00FF00", "0000FF"]
// White, Black, Red, Green, Blue

for hex in hexArr {
	fetchAPI(hexParam: hex)
}

func fetchAPI(hexParam: String) -> Void {

    let url = URL(string: "https://www.thecolorapi.com/id?hex=\(hexParam)")!
    var request = URLRequest(url: url)
    request.httpMethod = "GET"
    
    URLSession.shared.dataTask(with: request) { data, response, error in
        
        let json = try! JSONSerialization.jsonObject(with: data!) as! [String:Any]
        
        let name = json["name"] as! [String:Any]
        let nameValue = name["value"] as! String
        
        DispatchQueue.main.async {
            print(nameValue)
        }
                    
    }.resume()

}

We are hoping to get the result of:

White 
Black 
Red
Green
Blue

But instead it returned:

Black
White
Green
Red
Blue

We get all the colors right. But not in order…. If you try and run the code again, it will return in different order.

💡 This condition is called **race condition.**

Race condition is a situation where there are multiple tasks that are executed in the same time. Even though we’ve call the fetchAPI() in sequence of the array, but their completion time of each API calls are not the same.

As we know, each called URLSessionis running asynchronously. So if you need the looped call to be in order, you have to manipulate the API calls.


Manipulating the API Calls

URLSession will run other task asynchronously after .resume() is called next to the URLSession close bracket. So in theory, you need to call fetchAPI() before the closing bracket of the URLSession.

URLSession.shared.dataTask(with: request) { data, response, error in
	
    let json = try! JSONSerialization.jsonObject(with: data!) as! [String:Any]
    
    let name = json["name"] as! [String:Any]
    let nameValue = name["value"] as! String
    
    DispatchQueue.main.async {
        print(nameValue)
    }
    
    // MARK: The next API call should be here
				
}.resume()

The easiest way to implement this theory, is to use recursion. Because we can call the function again in the specified line.

Recursion Method

I personally use this method in my Nano Challenge 2 app, Colorio. This method is kinda ghetto though, because it is pure logic and doesn’t utilize Swift features like queues and semaphores. Performance might also be an issue because memory management in recursion isn’t known as the best.

Here’s how I implement the recursion.

  • Define how many times we want to execute the recursion
    • Set a base condition when to stop the recursion
      • We can use simple if-else or guard
      • We need to add an index parameter to mark when to stop
    • Recurse the function before the closing bracket
💡 Here the function will stop running after 5 function calls, which is the count of the array content
let hexArr = ["FFFFFF", "000000", "FF0000", "00FF00", "0000FF"]
// White, Black, Red, Green, Blue

// Defining when the recursion needs to stop based on the array count
let arrayCount = hexArr.count

// Calling the function
fetchAPI(index: 0) 

func fetchAPI(index: Int) -> Void {
    
    // Guarding the function to stop after it reaches the array count
    guard index < arrayCount else { return }

    let url = URL(string: "https://www.thecolorapi.com/id?hex=\(hexArr[index])")!
    var request = URLRequest(url: url)
    request.httpMethod = "GET"
    
    URLSession.shared.dataTask(with: request) { data, response, error in
        
        let json = try! JSONSerialization.jsonObject(with: data!) as! [String:Any]
        
        let name = json["name"] as! [String:Any]
        let nameValue = name["value"] as! String
        
        DispatchQueue.main.async {
            print(nameValue)
        }
        
        // Calling the recursion
        fetchAPI(index: index+1)
        
    }.resume()

}

As you can see, the result will always be consistent. But they will load a lot slower and one by one.

White 
Black 
Red
Green
Blue

Let’s try to implement the sequenced call using another way, semaphore.

Semaphore Method

Before we continue to the code implementation, here’s a bit of theory about semaphore I quoted from Roy Kronenfeld at Medium.

A semaphore consists of a threads queue and a counter value (type Int).

The threads queue is used by the semaphore to keep track of waiting threads in FIFO order (The first thread entered into the queue will be the first to get access to the shared resource once it is available).

The counter value is used by the semaphore to decide if a thread should get access to a shared resource or not. The counter value changes when we call signal() or wait() function.

So, when should we call wait() and signal() functions?

  • Call wait() each time before using the shared resource. We are basically asking the semaphore if the shared resource is available or not. If not, we will wait.
    • Call signal() each time after using the shared resource. We are basically signaling the semaphore that we are done interacting with the shared resource.

Code Implementation

Here are the steps we need to do to initialize the semaphore.

  • Declare a variable that contains DispatchSemaphore(value: Int)
    • let semaphore = DispatchSemaphore(value: 1)
    • Assign a value to the value parameter based on the queue quantity. Here, because we want to do it sequentially, we need to queue the array one by one. So assign 1 to the parameter. DispatchSemaphore(value: 1)
    • Wrap the API Call into a function. Here named fetchAPI(hexParam: String)
    • Create a loop calling the function
    • Assign semaphore.wait() to initiate the queue before calling fetchAPI(hexParam: String)
    • Assign semaphore.signal() to continue the queue before .resume()
let hexArr = ["FFFFFF", "000000", "FF0000", "00FF00", "0000FF"]
// White, Black, Red, Green, Blue

// Assign the semaphore variable
let semaphore = DispatchSemaphore(value: 1)

for hex in hexArr {
    semaphore.wait() // Initiate the queue
    fetchAPI(hexParam: hex)
}

func fetchAPI(hexParam: String) -> Void {

    let url = URL(string: "https://www.thecolorapi.com/id?hex=\(hexParam)")!
    var request = URLRequest(url: url)
    request.httpMethod = "GET"
    
    URLSession.shared.dataTask(with: request) { data, response, error in
        
        let json = try! JSONSerialization.jsonObject(with: data!) as! [String:Any]
        
        let name = json["name"] as! [String:Any]
        let nameValue = name["value"] as! String
        
        DispatchQueue.main.async {
            print(nameValue)
        }

        semaphore.signal() // Continue the queue
        
    }.resume()

}

Completion Handler

What if we want to reuse the function for a single API call? How can we remove the semaphore.signal() in our function while still able to pass the signal while chaining an API call like before?

We can use completion handlers for this kind of usage.

💡 A completion handler allows you to embed certain functions or lines of code anywhere in the function, not just at the end of the function call.

In dealing asynchronous tasks, we need to use @escaping in the parameter to wait for the async task to complete, then run our completion handler. If we don’t use @escapingthe handler will run immediately and won’t wait for async task to complete before running the handler.

Because we are putting the finished() handler inside a URLSession closure that is asynchronous, without the @escaping keyword, Xcode will return an error of:

expression failed to parse:
error: MyPlayground.playground:21:47: error: escaping closure captures non-escaping parameter 'finished'
    URLSession.shared.dataTask(with: request) { data, response, error in
                                              ^

MyPlayground.playground:15:47: note: parameter 'finished' is implicitly non-escaping
func fetchAPIUsingSemaphore(hexParam: String, finished: () -> Void) -> Void {
                                              ^

MyPlayground.playground:32:9: note: captured here
        finished()
        ^

So, here is the implementation example:

myFunction() {
    // Code to inject to the function
}

func myFunction(finished: @escaping() -> Void) -> Void {

    URLSession.shared.dataTask(with: request) { data, response, error in
    
        // Any process handling the data
        
        finished() // Put where you want any code injected to the function
        
    }.resume()

}

Here, we want to inject semaphore.signal() to the fetchAPI function before it resumes. So we can create a closure and put the signal inside the closure that will runs when the code reaches finished(). For a single API call, just call the function and give it an empty closure.

let hexArr = ["FFFFFF", "000000", "FF0000", "00FF00", "0000FF"]
// White, Black, Red, Green, Blue

let semaphore = DispatchSemaphore(value: 1)

// Looping and chaining the API Call
for hex in hexArr {
		semaphore.wait()
    fetchAPI(hexParam: hex) {
				// Injecting semaphore.signal to the function
        semaphore.signal()
    }
}

// Single API Call. Just give an empty closure
fetchAPI(hexParam: "FAFAFA"){}

func fetchAPI(hexParam: String, finished: @escaping() -> Void) -> Void {

    let url = URL(string: "https://www.thecolorapi.com/id?hex=\(hexParam)")!
    var request = URLRequest(url: url)
    request.httpMethod = "GET"
    
    URLSession.shared.dataTask(with: request) { data, response, error in
        
        let json = try! JSONSerialization.jsonObject(with: data!) as! [String:Any]
        
        let name = json["name"] as! [String:Any]
        let nameValue = name["value"] as! String
        
        DispatchQueue.main.async {
            print(nameValue)
        }
        
        finished()
        
    }.resume()

}

Voila, the race condition is dealt, and your API request will be fetched in sequence.


About My Nano Challenge 2 App, Colorio

Colorio is a color palette generator that offers daily suggestions for your perfect colory day. We suggest the perfect shades and artful combinations from your base, adding a sense of personal style. Then you can choose what you need - your base, or one of our great selection.

This application is mainly powered by two API provided by:

GitHub - gal-bert/Colorio: Apple Developer Academy - Nano Challenge 2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment