My first concurrent
app in Crystal

Lorenzo Barasti

My first concurrent
app in Crystal

1. Real-world problem

2. Abstract problem

3. CSP and Crystal

4. Putting things together

Intro

"max size: 1MB"

Intro - "max size: 1MB"

Characters

  • a bulky PDF
  • a form that won't accept it
  • the internet (chorus)

"Surely, someone has solved this before"

True

False

True

"There must be a web app for that"

True

False

False

"There must be an open source tool for that"

True

False

True

Part 1

"Hold my ginger beer"

An engineer

"It's as simple as that"

PDF

API

PDF

PDF

gs

PDF

PDF

Data flow

PDF

PDF

PDF

PDF

PDF

PDF

PDF

PDF

Request handlers

Compressors

Publisher

Data flow

Output

Producers

Processors

Collector

A pipeline, indeed!

Part 2

Communicating Sequential
Processes

CSP

A concurrent paradigm where
Processes communicate
by passing messages
over channels

CSP in Crystal

A concurrent paradigm where
Processes Fibers communicate
by passing messages
over Channels

CSP in Crystal

A concurrent paradigm where
Processes Fibers communicate
by passing messages
over channels

Fibers

Lightweight

Cooperative

Unit of execution

CSP in Crystal

A concurrent paradigm where
Processes Fibers communicate
by passing messages
over Channels

Channels

blocking #send and #receive

similar to queues

Regulate data exchange between fibers

Fibers

Channel

Message

CSP in Crystal

spawn do
  # your concurrent task!
end

To start a Fiber we

 

CSP in Crystal

ch = Channel(Job).new

Creating a Channel

 

Sending & receiving

 

ch.send(Msg.new(uuid))
msg = ch.receive

Would you like to see some code?

Yes

No

Yes

CSP in Crystal

ch = Channel(Job).new
msg = Msg.new(uuid)

ch.send msg
puts ch.receive

CSP in Crystal

ch = Channel(Job).new
msg = Msg.new(uuid)

ch.send msg
puts ch.receive

CSP in Crystal

ch = Channel(Job).new
msg = Msg.new(uuid)

ch.send msg
puts ch.receive

CSP in Crystal

ch = Channel(Job).new
msg = Msg.new(uuid)

ch.send msg
puts ch.receive

CSP in Crystal

ch = Channel(Job).new
msg = Msg.new(uuid)

ch.send msg
puts ch.receive

X

CSP in Crystal

ch = Channel(Job).new
msg = Msg.new(uuid)

ch.send msg
puts ch.receive

X

CSP in Crystal

When a Fiber blocks, it yields control to another Fiber.

 

 

Fiber.yield
sleep 2
HTTP.get(url)
ch.receive
ch.send obj

Crystal's execution model

  • Scheduler: allocates time for fibers to run
  • Event loop: a fiber that coordinates async tasks
  • Garbage collector: clears unused memory - runs on a separate thread

Crystal's execution model

fiber A

Thread

Scheduler

Crystal's execution model

Thread

Scheduler

fiber A

fiber B

Crystal's execution model

Scheduler

Thread

fiber A

fiber B

spawn(name: "fiber A") do
  spawn(name: "fiber Z") do
    # do something
  end
  msg = ch.receive
end

fiber Z

Crystal's execution model

Scheduler

Thread

fiber A

fiber B

spawn(name: "fiber A") do
  spawn(name: "fiber Z") do
    # do something
  end
  msg = ch.receive
end

fiber Z

Crystal's execution model

Scheduler

Thread

fiber A

Thread

fiber B

spawn(name: "fiber A") do
  spawn(name: "fiber Z") do
    # do something
  end
  msg = ch.receive
end

fiber Z

Crystal's execution model

Scheduler

Thread

fiber A

Thread

fiber B

fiber Z

Crystal's execution model

Scheduler

Thread

Thread

fiber B

fiber A

fiber Z

Crystal's execution model

Scheduler

Thread

fiber A

Thread

fiber B

fiber C

fiber Z

Crystal's execution model

Scheduler

Thread

Thread

fiber C

fiber A

fiber B

spawn(name: "fiber B") do
  Fiber.yield
end

fiber Z

Crystal's execution model

Scheduler

Thread

Thread

fiber C

fiber A

fiber B

fiber Z

Crystal's execution model

Scheduler

Thread

Thread

fiber C

fiber A

fiber B

fiber Z

CSP in Crystal

ch = Channel(Job).new
msg = Msg.new(uuid)

spawn(name: "processor") do
  puts ch.receive
end

ch.send msg

CSP in Crystal

ch = Channel(Job).new
msg = Msg.new(uuid)

spawn(name: "processor") do
  puts ch.receive
end

ch.send msg

CSP in Crystal

ch = Channel(Job).new
msg = Msg.new(uuid)

spawn(name: "processor") do
  puts ch.receive
end

ch.send msg

CSP in Crystal

ch = Channel(Job).new
msg = Msg.new(uuid)

spawn(name: "processor") do
  puts ch.receive
end

ch.send msg

CSP in Crystal

ch = Channel(Job).new
msg = Msg.new(uuid)

spawn(name: "processor") do
  puts ch.receive
end

ch.send msg

X

CSP in Crystal

ch = Channel(Job).new
msg = Msg.new(uuid)

spawn(name: "processor") do
  puts ch.receive
end

ch.send msg

X

CSP in Crystal

ch = Channel(Job).new
msg = Msg.new(uuid)

spawn(name: "processor") do
  puts ch.receive
end

ch.send msg

X

CSP in Crystal

ch = Channel(Job).new
msg = Msg.new(uuid)

spawn(name: "processor") do
  puts ch.receive
end

ch.send msg

CSP in Crystal

ch = Channel(Job).new
msg = Msg.new(uuid)

spawn(name: "processor") do
  puts ch.receive
end

ch.send msg

Termination

CSP in Crystal

ch = Channel(Job).new

2.times.each { |i|
  spawn(name: "processor_#{i}") do
    loop do
      puts "#{Fiber.current.name}: #{ch.receive}"
    end
  end
}

loop do
  ch.send Msg.new(UUID.random)
  sleep 0.5
end

CSP in Crystal

ch = Channel(Job).new

2.times.each { |i|
  spawn(name: "processor_#{i}") do
    loop do
      puts "#{Fiber.current.name}: #{ch.receive}"
    end
  end
}

loop do
  ch.send Msg.new(UUID.random)
  sleep 0.5
end

CSP in Crystal

ch = Channel(Job).new

2.times.each { |i|
  spawn(name: "processor_#{i}") do
    loop do
      puts "#{Fiber.current.name}: #{ch.receive}"
    end
  end
}

loop do
  ch.send Msg.new(UUID.random)
  sleep 0.5
end

CSP in Crystal

ch = Channel(Job).new

2.times.each { |i|
  spawn(name: "processor_#{i}") do
    loop do
      puts "#{Fiber.current.name}: #{ch.receive}"
    end
  end
}

loop do
  ch.send Msg.new(UUID.random)
  sleep 0.5
end

X

CSP in Crystal

ch = Channel(Job).new

2.times.each { |i|
  spawn(name: "processor_#{i}") do
    loop do
      puts "#{Fiber.current.name}: #{ch.receive}"
    end
  end
}

loop do
  ch.send Msg.new(UUID.random)
  sleep 0.5
end

X

CSP in Crystal

ch = Channel(Job).new

2.times.each { |i|
  spawn(name: "processor_#{i}") do
    loop do
      puts "#{Fiber.current.name}: #{ch.receive}"
    end
  end
}

loop do
  ch.send Msg.new(UUID.random)
  sleep 0.5
end

X

X

CSP in Crystal

ch = Channel(Job).new

2.times.each { |i|
  spawn(name: "processor_#{i}") do
    loop do
      puts "#{Fiber.current.name}: #{ch.receive}"
    end
  end
}

loop do
  ch.send Msg.new(UUID.random)
  sleep 0.5
end

X

X

CSP in Crystal

ch = Channel(Job).new

2.times.each { |i|
  spawn(name: "processor_#{i}") do
    loop do
      puts "#{Fiber.current.name}: #{ch.receive}"
    end
  end
}

loop do
  ch.send Msg.new(UUID.random)
  sleep 0.5
end

X

X

CSP in Crystal

ch = Channel(Job).new

2.times.each { |i|
  spawn(name: "processor_#{i}") do
    loop do
      puts "#{Fiber.current.name}: #{ch.receive}"
    end
  end
}

loop do
  ch.send Msg.new(UUID.random)
  sleep 0.5
end

X

X

CSP in Crystal

ch = Channel(Job).new

2.times.each { |i|
  spawn(name: "processor_#{i}") do
    loop do
      puts "#{Fiber.current.name}: #{ch.receive}"
    end
  end
}

loop do
  ch.send Msg.new(UUID.random)
  sleep 0.5
end

X

X

X

CSP in Crystal

ch = Channel(Job).new

2.times.each { |i|
  spawn(name: "processor_#{i}") do
    loop do
      puts "#{Fiber.current.name}: #{ch.receive}"
    end
  end
}

loop do
  ch.send Msg.new(UUID.random)
  sleep 0.5
end

X

X

CSP in Crystal

ch = Channel(Job).new

2.times.each { |i|
  spawn(name: "processor_#{i}") do
    loop do
      puts "#{Fiber.current.name}: #{ch.receive}"
    end
  end
}

loop do
  ch.send Msg.new(UUID.random)
  sleep 0.5
end

X

X

X

CSP in Crystal

ch = Channel(Job).new

2.times.each { |i|
  spawn(name: "processor_#{i}") do
    loop do
      puts "#{Fiber.current.name}: #{ch.receive}"
    end
  end
}

loop do
  ch.send Msg.new(UUID.random)
  sleep 0.5
end

X

X

CSP in Crystal

ch = Channel(Job).new

2.times.each { |i|
  spawn(name: "processor_#{i}") do
    loop do
      puts "#{Fiber.current.name}: #{ch.receive}"
    end
  end
}

loop do
  ch.send Msg.new(UUID.random)
  sleep 0.5
end

X

X

X

CSP in Crystal

ch = Channel(Job).new

2.times.each { |i|
  spawn(name: "processor_#{i}") do
    loop do
      puts "#{Fiber.current.name}: #{ch.receive}"
    end
  end
}

loop do
  ch.send Msg.new(UUID.random)
  sleep 0.5
end

X

X

CSP in Crystal

ch = Channel(Job).new

2.times.each { |i|
  spawn(name: "processor_#{i}") do
    loop do
      puts "#{Fiber.current.name}: #{ch.receive}"
    end
  end
}

loop do
  ch.send Msg.new(UUID.random)
  sleep 0.5
end

X

X

Interlude

Pipelines everywhere

Pipelines everywhere

Pipelines everywhere

To share state we communicate via channels

 

Job_id_stream = Channel(Int32).new

spawn(name: "job_id_generator") do
  i = 0
  loop do
    Job_id_stream.send i
    i += 1
  end
end

def handle_request(request)
  job_id = Job_id_stream.receive
  # ...
end

Pipelines everywhere

To share state we communicate via channels

 

Job_id_stream = Channel(Int32).new

spawn(name: "job_id_generator") do
  i = 0
  loop do
    Job_id_stream.send i
    i += 1
  end
end

def handle_request(request)
  job_id = Job_id_stream.receive
  # ...
end

Pipelines everywhere

To share state we communicate via channels

 

Job_id_stream = Channel(Int32).new

spawn(name: "job_id_generator") do
  i = 0
  loop do
    Job_id_stream.send i
    i += 1
  end
end

def handle_request(request)
  job_id = Job_id_stream.receive
  # ...
end

Pipelines everywhere

To share state we communicate via channels

 

Job_id_stream = Channel(Int32).new

spawn(name: "job_id_generator") do
  i = 0
  loop do
    Job_id_stream.send i
    i += 1
  end
end

def handle_request(request)
  job_id = Job_id_stream.receive
  # ...
end

Pipelines everywhere

Job_stream = Channel(Job).new

def handle_request(request)
  params = extract_params(request)
  job_id = Job_id_stream.receive

  persist(job_id, params.file)
  job = Job.new(job_id, params.options)

  Job_stream.send(job)
end

WORKERS.each {
  spawn do
    loop do
      process(Job_stream.receive)
    end
  end
}

Pipelines everywhere

Job_stream = Channel(Job).new

def handle_request(request)
  params = extract_params(request)
  job_id = Job_id_stream.receive

  persist(job_id, params.file)
  job = Job.new(job_id, params.options)

  Job_stream.send(job)
end

WORKERS.each {
  spawn do
    loop do
      process(Job_stream.receive)
    end
  end
}

Pipelines everywhere

Job_stream = Channel(Job).new

def handle_request(request)
  params = extract_params(request)
  job_id = Job_id_stream.receive

  persist(job_id, params.file)
  job = Job.new(job_id, params.options)

  Job_stream.send(job)
end

WORKERS.each {
  spawn do
    loop do
      process(Job_stream.receive)
    end
  end
}

Pipelines everywhere

Part 3

Wrapping up!

Wrapping up

Fibers

Channels

Success!

{
  name: lorenzo.barasti,
  live_codes_on: twitch.com/lbarasti,
  blogs_at: lbarasti.com,
  tweets_as: @lbarasti
}

My first concurrent
app in Crystal

Thanks for listening!

My first concurrent
app in Crystal

My first concurrent app in Crystal

By Lorenzo Barasti

My first concurrent app in Crystal

We look into Crystal's concurrency model (CSP) and how that shapes the way we write concurrent applications

  • 761