Swift 3 Linux tutorial - part 2

In this tutorial we will continue exploring more advanced parts of Zewo framework, including:

  • concurrency
  • JSON handling
  • communication via channels

Coroutines

Unlike Go, Swift 3 does not come with coroutines out of the box, but uses separate library called VeniceX. Worry no, the usage is quite similar to go, and performance is great, scaling up to milions coroutines on a single host.

Zewo follows CSP model, similar to Golang read more model coroutines incorporting wonderful libmill C library.

Example coroutines

This code runs in the “background” as a lightweight coroutine. Unlike threads, you can run literally milions of these on the modern hardware, it’s so performant.

import Venice

for i in 1...100 {
	co {
		print("Spawned \(i) coroutine! I'm so asynchronous!")
	}
}

To comunicate with the coroutine, we use channels, for sending / recieving values of specific type(for example Int):

import Venice

let channel = Channel<Int>()

co {
	nap(for: 1.second)
	channel.send(1)
}

print("Waiting for the result...")
let value = channel.receive()
print("Got \(value) !")

Run it and:

Waiting for the result...
Got Optional(1) !

Keep in mind, that recieving a value from the channel is a blocking operation. To check buffer for presence of data without blocking, you can use select function described later on.

DIY - worker

In the first part of the tutorial, we’ve built synchronous http service which immediately returned a value. In this tutorial, we will try to make prime numbers web-service! Computing of prime numbers takes a lot time, and we don’t want to have API HTTP timeouts. That’s why we cannot do it synchronusly. We will defer computation tasks to the background instead.

How to do it? Do we need some worker solution, similar to Python RQ, Celery, threads, Redis or something? The answer is - not at all! Zewo with Swift does the trick without any other components!

Let’s begin with the endpoints:

  • /task/new endpoint will schedule long-running computation in the background. The only thing returned synchronusly is task id as a JSON. We handle POST requests here, thus “creating” task resource.
  • /task/[uid] - this endpoint will be used by the clients polling the result.

The code

File main.swift:


import Axis
import HTTPServer
import Foundation

var messages:[String:Channel<Int>] = [:]


let app = BasicRouter { route in

    route.post("/task/new") { request in 
        var body = request.body
        let data = try body.becomeBuffer(deadline: 30.seconds)
        let content = try JSONMapParser().parse(data)
        guard let value = content?["value"].string else {
            return Response(status: .badRequest, body: "Missing value")
        }
        guard let intValue = Int(value) else {
            return Response(status: .badRequest, body: "\(value) is not a number")
        }

        let taskId = UUID().description
        let channel = Channel<Int>()
        messages[taskId] = channel

        co {
            let computedPrime = nextPrime(intValue)
            channel.send(computedPrime)
        }

        return Response(
            headers: ["Content-type": "application/json"],
            body: try! JSONMapSerializer.serialize(["taskId": .string(taskId)])
        )
    }

    route.get("/task/:taskid") { request in
        guard let taskid = request.pathParameters["taskid"] else {
            return Response(status: .internalServerError)
        }
        guard let channel = messages[taskid] else {
            return Response(
                status: .notFound,
                body: "Routine not found"
            )
        }
        var response: Response?
        select { when in
            when.receive(from: channel) { message in
                response = Response(body: "Result: \(message)")
                messages[taskid] = nil
            }
            when.otherwise {
                response = Response(body: "Still computing.")
            }
        }
        return response!
    }
}

try Server(host: "0.0.0.0", port: 8080, reusePort: true, responder: app).start()

Create file computation.swift:

import Venice

/// Returns true if number is prime number, false otherwise
func isPrime(_ number: Int) -> Bool {
    guard number != 1 else {
        return false 
    }
    var prime = true
    var i = 2
    while i < number {
        if i % 1000 == 0 {
            yield
        }
        if number % i == 0 {
            prime = false
            break
        }
        i = i + 1
    }
    return prime
}

/// Returns the first next prime number greater than given number
func nextPrime(_ previousPrime: Int) -> Int {
        var currentCandidate = previousPrime
	var found = false
	while found == false {
	    currentCandidate += 1
	    if isPrime(currentCandidate) == true {
	        found = true
	    }
	}
	return currentCandidate
}

Both files(main.swift, computation.swift) share common namespace, without a need for importing files each other.

Now, compile the code and let’s try our computation.

> curl -XPOST  "http://127.0.0.1:8080/task/new" -d '{"value": "198491329"}' -v

{"taskId":"7398EFF1-838A-495E-AB9C-AC7C5C73BE3"}

> curl "http://127.0.0.1:8080/task/7398EFF1-838A-495E-AB9C-AC7C5C73BE3"

Still computing.

[after a while]

> curl "http://127.0.0.1:8080/task/7398EFF1-838A-495E-AB9C-AC7C5C73BE3"
Result: 198491369⏎                      

Explanation

The essence of our program is the usage of this coroutine:


co {
    let computedPrime = nextPrime(intValue)
    channel.send(computedPrime)
}
  • nextPrime() function tries to behave nice, yielding the control of program to other coroutines, with the yield function. Without it, it would preempt entire CPU time for one coroutine.

  • using guards to unpack Optionals makes a program really readable

  • we used select() function which doesn’t wait until channel is filled up, so we can always answer HTTP requests as fast as possible

Summary

Congratulations! You completed part 2 of the tutorial. Don’t miss part 3 with Filesystem functions!