Dealing With Race Conditions In Chained API Calls Using Swift | by Gregorius Albert

Completion handlers, recursion and semaphores explained using examples

In this article, I am going to share some of my experience while handling chained API calls in my Nano Challenge 2 application Colorio. Here I’ll talk about my goto ways of handling them, and also the alternatives that can be used.

  • closing handler
  • repatriation
  • semaphores

Have you ever wanted to use an API and load their data into a table view? Typically you would do this in your view controller:

Example JSON data received from API:

[

"id": 1,
"author": "Gregorius Albert",
"content": "This is my first tweet"
,

"id": 2,
"author": "Taylor Swift",
"content": "It's August baby"
,

"id": 3,
"author": "Justin Bieber",
"content": "What's about the Baby song thing?"

]

Here is the code for fetching data from API and loading it on table view:

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 data in bulk or in collections. it’s like doing a SELECT * in a SQL database.

But what if you’re using a public API that doesn’t provide bulk data? Some APIs allow you to get only a single data based on a parameter. See 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 which I have given in URL which is E62028, But I need to get the data for multiple colors at once. how can i do that?

Well, many people may think “just loop the api call”, Well, you technically can run API calls that way. Let’s try to loop through the 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 expecting 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 to run the code again, it will come back in a different order.

💡 This condition is called race condition.

A race condition is a situation where multiple tasks are executed at the same time. Although we have called fetchAPI() In sequence to the array, but the completion time of each API call is not the same.

As we know, each called URLSession running asynchronously. So if you need looped calls, you have to manipulate the API calls.

Here, URLSession will run other tasks asynchronously after .resume() is called near URLSession closing parenthesis. So in theory, you need to call fetchAPI() before the closing bracket of 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, in principle, is to use recursion. Because we can call the function again in the specified line.

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

Here’s how I implement the recursion:

  • Set base position to prevent recursion
  • we can use simple if-else either guard
  • we need to add a index parameter to mark when to stop
  • Recurse the function before the closing bracket

💡 Here the function will stop running after 5 function calls, that is counting the array contents

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 very slowly and one by one.

White 
Black
Red
Green
Blue

Let’s try to implement sequenced calls in another way, using semaphores.

Before we continue with the code implementation, here’s a little bit of theory about semaphores piece by roy kronenfeld To speed you up:

A semaphore consists of a thread queue and a counter value (of type int).

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

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

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

– Source: Roy Kronenfeld

To initialize the semaphore we need to do these steps:

  • let semaphore = DispatchSemaphore(value: 1)
  • Assign a value to the value parameter based on the queue quantity. Here, since we want to do it sequentially, we need to queue up the arrays one by one. So assign 1 to the parameter. DispatchSemaphore(value: 1)
  • Wrap the API call in a function. named here fetchAPI(hexParam: String)
  • create a loop that calls the function
  • assign semaphore.wait() to initialize the queue before calling fetchAPI(hexParam: String)
  • assign semaphore.signal() to continue the first queue .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()

What if we want to reuse a function for a single API call? how can we remove semaphore.signal() Is our function still able to pass signals while chasing API calls as before?

We can use completion operators for such usage.

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

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

because we are putting finished() handler inside URLSession closure which is asynchronous, without@escaping keyword, Xcode will return the error:

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 fetchAPI before resuming work.

So we can create a closure and put a signal inside the closure which will run when the code is reached finished().

For a single API call, just call the function and close it empty.

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 with, and your API request will be received in order.

Thank you for reading.

Leave a Reply