Skip to content

Appsignal::CheckIn.scheduler isn't thread-safe but looks like it is intended to be #1343

@Burgestrand

Description

@Burgestrand

Hi! It might not actually be a problem to have multiple schedulers running. I'm just looking through the code to debug why I'm missing check-in events when using .cron(...) { block }, and spotted this.

TLDR: it's possible for there to be multiple Appsignal::CheckIn::Scheduler instances running, and it keeps itself alive.

def scheduler
@scheduler ||= Scheduler.new
end

It's possible to spawn multiple schedulers if e.g. you call Appsignal::CheckIn.cron(...) from separate threads, if there's a context-switch during the execution of the ||= operator.

Here's an example script to highlight the behaviour:

example script
require "appsignal"

Event = Data.define(:thread, :event, :scheduler)

module Example
  # Simulate a delay in the initialization of the scheduler.
  module DelayInitialization
    def initialize(...)
      sleep(0.5)
      super
    end
  end
  Appsignal::CheckIn::Scheduler.prepend(DelayInitialization)

  @events = Thread::Queue.new
  class << self
    attr_reader :events

    def mark!(event)
      events << Event.new(Thread.current.name, event, Appsignal::CheckIn.scheduler)
    end

    def count_schedulers(id)
      schedulers = ObjectSpace.each_object(Appsignal::CheckIn::Scheduler)
      $stdout.puts "#{id}: #{schedulers.count}"
    end
  end
end

Example.count_schedulers("Before.")

Appsignal.start
threads = 2.times.map { |index|
  Thread.new do
    Thread.current.name = "Thread #{index}"
    Example.mark!("start")
    Appsignal::CheckIn.cron("thr1") do
      Example.mark!("checking in")
      sleep 1
    end
    Example.mark!("done")
  end
}

threads.each(&:join)
Example.events.close
Example.count_schedulers("After.")

$stdout.puts "Events:"
Example.events.size.times do
  event = Example.events.pop
  $stdout.puts "#{event.thread}: #{event.event} (#{event.scheduler&.__id__})"
end

Example output:

Before.: 0
After.: 2
Events:
Thread 0: start (680)
Thread 0: checking in (680)
Thread 1: start (700)
Thread 1: checking in (700)
Thread 0: done (700)
Thread 1: done (700)

Workaround

Calling Appsignal::CheckIn.scheduler during application boot should be sufficient.

### Tasks

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions