Skip to content

Commit

Permalink
Fix dead-lock on Linux
Browse files Browse the repository at this point in the history
  • Loading branch information
ffried committed Oct 1, 2023
1 parent 379b98f commit 00067a9
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 120 deletions.
30 changes: 30 additions & 0 deletions Sources/SwiftyHolidays/CalculationCalendar.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation

// Workaround for Calendar not being thread-safe on linux

@frozen
@usableFromInline
struct CalculationCalendar {
@usableFromInline
let calendar: Calendar
#if !canImport(Darwin)
@usableFromInline
let calendarLock = DispatchQueue(label: "de.sersoft.swiftyholidays.calculation-calendar.lock")
#endif

@inlinable
func withCalendar<T>(do work: (Calendar) throws -> T) rethrows -> T {
#if canImport(Darwin)
try work(calendar)
#else
dispatchPrecondition(condition: .notOnQueue(calendarLock))
return try calendarLock.sync { try work(calendar) }
#endif
}
}

#if canImport(Darwin)
extension CalculationCalendar: Sendable {}
#else
extension CalculationCalendar: @unchecked Sendable {}
#endif
9 changes: 0 additions & 9 deletions Sources/SwiftyHolidays/CalculationContextReference.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,6 @@ final class CalculationContextReference<Context: CalculationContext>: @unchecked
return try contextLock.sync(flags: .barrier) { try work(&_context) }
}

/// Gives limited mutating access to the context inside the reference.
/// - Parameter work: The work to be executed that can mutate the context.
/// - Throws: Any error thrown by `work`.
/// - SeeAlso: `withContext(do:)`
@inlinable
func withContextVoid(do work: (inout Context) throws -> ()) rethrows {
try withContext(do: work)
}

/// Exchanges the stored context with a new one, returning the old one.
/// - Parameter context: The new context to replace the old with.
/// - Returns: The old context that was previously stored in the reference.
Expand Down
140 changes: 39 additions & 101 deletions Sources/SwiftyHolidays/Gregorian/GregorianCalculator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import Foundation
public struct GregorianCalculator: Calculator {
public typealias Context = GregorianCalculationContext

#if !canImport(Darwin)
@usableFromInline
let calendarLock = DispatchQueue(label: "de.sersoft.swiftyholidays.calendar.lock")
#endif
public let calendar: Calendar
let calculationCalendar: CalculationCalendar

@inlinable
public var calendar: Calendar {
calculationCalendar.withCalendar { $0 }
}

/// The reference to the context.
@usableFromInline
Expand All @@ -21,16 +23,6 @@ public struct GregorianCalculator: Calculator {
@inlinable
public var context: Context { contextRef.context }

@inlinable
func _withCalendar<T>(do work: (Calendar) throws -> T) rethrows -> T {
#if canImport(Darwin)
try work(calendar)
#else
dispatchPrecondition(condition: .notOnQueue(calendarLock))
return try calendarLock.sync { try work(calendar) }
#endif
}

/// Creates a new gregorian calculator (also using a new context).
public init() {
var calendar = Calendar(identifier: .gregorian)
Expand All @@ -43,7 +35,7 @@ public struct GregorianCalculator: Calculator {
#else
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
#endif
self.calendar = calendar
calculationCalendar = .init(calendar: calendar)
}

@inlinable
Expand All @@ -52,20 +44,12 @@ public struct GregorianCalculator: Calculator {
oldCtx.clear(keepingCapacity: false) // signal any leftover semaphores
}

/// Returns the date for a given context storage key in a given year by either using the already calculated and cached result,
/// or calculating it by executing the given calculation closure.
/// - Parameters:
/// - key: The key for which to return the date.
/// - year: The year for which to return the date.
/// - calculation: The calculation that would calculate the date if none exists yet.
/// - Returns: The date that was either cached or calculated.
/// - Note: This method also waits on existing calculations on other threads.
@usableFromInline
/*private but @usableFromInline*/ func date(for key: Context.StorageKey,
forYear year: Int,
calculation: (Calendar, Int) -> HolidayDate) -> HolidayDate {
calculation: (CalculationCalendar, Int) -> HolidayDate) -> HolidayDate {
@inline(__always)
func wait(for promise: CalculationPromise<HolidayDate>, calculation: (Calendar, Int) -> HolidayDate) -> HolidayDate {
func wait(for promise: CalculationPromise<HolidayDate>, calculation: (CalculationCalendar, Int) -> HolidayDate) -> HolidayDate {
switch promise {
case .waiting(let sema):
sema.wait()
Expand All @@ -77,159 +61,113 @@ public struct GregorianCalculator: Calculator {
if let promise = context[key, forYear: year] { return wait(for: promise, calculation: calculation) }
let promise = contextRef.withContext { $0[storedFor: key, forYear: year] }
if promise.wasCreated {
let calculated = _withCalendar { calculation($0, year) }
contextRef.withContextVoid { $0.fulfill(key, with: calculated) }
let calculated = calculation(calculationCalendar, year)
contextRef.withContext { $0.fulfill(key, with: calculated) }
return calculated
} else {
return wait(for: promise.0, calculation: calculation)
}
}

/// Calculates a date by adding a certain amount of days to the result of another calculation.
/// - Parameters:
/// - days: The number of days to add to the result of `otherDate`. Can be negative.
/// - otherDate: The calculation closure to execute to retrieve the date to add `days` to.
/// - year: The year for which to perform these calculations.
/// - Returns: The date after adding `days` to the date returned by `otherDate`.
@inlinable
/*private but @inlinable*/ func calculateByAdding(days: Int,
toResultOf otherDate: (Int) -> HolidayDate,
forYear year: Int) -> HolidayDate {
var otherDateComps = otherDate(year).components
otherDateComps.day! += days
return _withCalendar {
return calculationCalendar.withCalendar {
let date = $0.date(from: otherDateComps)!
return HolidayDate(date: date, in: $0)
}
}

/// Calculates the palm sunday date for a given year.
/// - Parameter year: The year for which to calculate palm sunday.
/// - Returns: The calculated date for palm sunday.
@inlinable
func calculatePalmSunday(in calendar: Calendar, forYear year: Int) -> HolidayDate {
func calculatePalmSunday(in calendar: CalculationCalendar, forYear year: Int) -> HolidayDate {
calculateByAdding(days: -7, toResultOf: easterSunday, forYear: year)
}

/// Calculates the maundy thursday date for a given year.
/// - Parameter year: The year for which to calculate maundy thursday.
/// - Returns: The calculated date for maundy thursday.
@inlinable
func calculateMaundyThursday(in calendar: Calendar, forYear year: Int) -> HolidayDate {
func calculateMaundyThursday(in calendar: CalculationCalendar, forYear year: Int) -> HolidayDate {
calculateByAdding(days: -3, toResultOf: easterSunday, forYear: year)
}

/// Calculates the good friday date for a given year.
/// - Parameter year: The year for which to calculate good friday.
/// - Returns: The calculated date for good friday.
@inlinable
func calculateGoodFriday(in calendar: Calendar, forYear year: Int) -> HolidayDate {
func calculateGoodFriday(in calendar: CalculationCalendar, forYear year: Int) -> HolidayDate {
calculateByAdding(days: -2, toResultOf: easterSunday, forYear: year)
}

/// Calculates the holy saturday date for a given year.
/// - Parameter year: The year for which to calculate the holy saturday.
/// - Returns: The calculated date for holy saturday.
@inlinable
func calculateHolySaturday(in calendar: Calendar, forYear year: Int) -> HolidayDate {
func calculateHolySaturday(in calendar: CalculationCalendar, forYear year: Int) -> HolidayDate {
calculateByAdding(days: -1, toResultOf: easterSunday, forYear: year)
}

/// Calculates the easter sunday date for a given year.
/// - Parameter year: The year for which to calculate easter sunday.
/// - Returns: The calculated date for easter sunday.
@usableFromInline
func calculateEasterSunday(in calendar: Calendar, forYear year: Int) -> HolidayDate {
func calculateEasterSunday(in calendar: CalculationCalendar, forYear year: Int) -> HolidayDate {
let d = (19 * (year % 19) + 24) % 30
let e = (2 * (year % 4) + 4 * (year % 7) + 6 * d + 5) % 7
let p = 22 + d + e
let comps = DateComponents(year: year, month: 3, day: p)
let date = calendar.date(from: comps)!
return HolidayDate(date: date, in: calendar)
return calendar.withCalendar {
let date = $0.date(from: comps)!
return HolidayDate(date: date, in: $0)
}
}

/// Calculates the easter monday date for a given year.
/// - Parameter year: The year for which to calculate easter monday.
/// - Returns: The calculated date for easter monday.
@inlinable
func calculateEasterMonday(in calendar: Calendar, forYear year: Int) -> HolidayDate {
func calculateEasterMonday(in calendar: CalculationCalendar, forYear year: Int) -> HolidayDate {
calculateByAdding(days: 1, toResultOf: easterSunday, forYear: year)
}

/// Calculates the ascension day date for a given year.
/// - Parameter year: The year for which to calculate the ascension day.
/// - Returns: The calculated date for ascension day.
@inlinable
func calculateAscensionDay(in calendar: Calendar, forYear year: Int) -> HolidayDate {
func calculateAscensionDay(in calendar: CalculationCalendar, forYear year: Int) -> HolidayDate {
calculateByAdding(days: 39, toResultOf: easterSunday, forYear: year)
}

/// Calculates the pentecost date for a given year.
/// - Parameter year: The year for which to calculate pentecost.
/// - Returns: The calculated date for pentecost.
@inlinable
func calculatePentecost(in calendar: Calendar, forYear year: Int) -> HolidayDate {
func calculatePentecost(in calendar: CalculationCalendar, forYear year: Int) -> HolidayDate {
calculateByAdding(days: 49, toResultOf: easterSunday, forYear: year)
}

/// Calculates the whit monday date for a given year.
/// - Parameter year: The year for which to calculate whit monday.
/// - Returns: The calculated date for whit monday.
@inlinable
func calculateWhitMonday(in calendar: Calendar, forYear year: Int) -> HolidayDate {
func calculateWhitMonday(in calendar: CalculationCalendar, forYear year: Int) -> HolidayDate {
calculateByAdding(days: 50, toResultOf: easterSunday, forYear: year)
}

/// Calculates the corpus christi date for a given year.
/// - Parameter year: The year for which to calculate corpus christi.
/// - Returns: The calculated date for corpus christi.
@inlinable
func calculateCorpusChristi(in calendar: Calendar, forYear year: Int) -> HolidayDate {
func calculateCorpusChristi(in calendar: CalculationCalendar, forYear year: Int) -> HolidayDate {
calculateByAdding(days: 60, toResultOf: easterSunday, forYear: year)
}

/// Calculates the sunday after corpus christi date for a given year.
/// - Parameter year: The year for which to calculate the sunday after corpus christi.
/// - Returns: The calculated date for sunday after corpus christi.
@inlinable
func calculateSundayAfterCorpusChristi(in calendar: Calendar, forYear year: Int) -> HolidayDate {
func calculateSundayAfterCorpusChristi(in calendar: CalculationCalendar, forYear year: Int) -> HolidayDate {
calculateByAdding(days: 63, toResultOf: easterSunday, forYear: year)
}

/// Calculates the first sunday of advent date for a given year.
/// - Parameter year: The year for which to calculate the first sunday of advent.
/// - Returns: The calculated date for first sunday of advent.
@inlinable
func calculateFirstSundayOfAdvent(in calendar: Calendar, forYear year: Int) -> HolidayDate {
func calculateFirstSundayOfAdvent(in calendar: CalculationCalendar, forYear year: Int) -> HolidayDate {
calculateByAdding(days: -21, toResultOf: fourthSundayOfAdvent, forYear: year)
}

/// Calculates the second sunday of advent date for a given year.
/// - Parameter year: The year for which to calculate the second sunday of advent.
/// - Returns: The calculated date for second sunday of advent.
@inlinable
func calculateSecondSundayOfAdvent(in calendar: Calendar, forYear year: Int) -> HolidayDate {
func calculateSecondSundayOfAdvent(in calendar: CalculationCalendar, forYear year: Int) -> HolidayDate {
calculateByAdding(days: -14, toResultOf: fourthSundayOfAdvent, forYear: year)
}

/// Calculates the third sunday of advent date for a given year.
/// - Parameter year: The year for which to calculate the third sunday of advent.
/// - Returns: The calculated date for third sunday of advent.
@inlinable
func calculateThirdSundayOfAdvent(in calendar: Calendar, forYear year: Int) -> HolidayDate {
func calculateThirdSundayOfAdvent(in calendar: CalculationCalendar, forYear year: Int) -> HolidayDate {
calculateByAdding(days: -7, toResultOf: fourthSundayOfAdvent, forYear: year)
}

/// Calculates the fourth sunday of advent date for a given year.
/// - Parameter year: The year for which to calculate the fourth sunday of advent.
/// - Returns: The calculated date for fourth sunday of advent.
@usableFromInline
func calculateFourthSundayOfAdvent(in calendar: Calendar, forYear year: Int) -> HolidayDate {
let christmas = christmasDay(forYear: year).date(in: calendar)!
let mondayAfter = (calendar.dateIntervalOfWeekend(containing: christmas)?.end
?? calendar.nextWeekend(startingAfter: christmas, direction: .backward)?.end)!
let sunday = calendar.date(byAdding: .day, value: -1, to: mondayAfter)!
return HolidayDate(date: sunday, in: calendar)
func calculateFourthSundayOfAdvent(in calendar: CalculationCalendar, forYear year: Int) -> HolidayDate {
let christmasDay = christmasDay(forYear: year)
return calendar.withCalendar {
let christmasDate = christmasDay.date(in: $0)!
let mondayAfter = ($0.dateIntervalOfWeekend(containing: christmasDate)?.end
?? $0.nextWeekend(startingAfter: christmasDate, direction: .backward)?.end)!
let sunday = $0.date(byAdding: .day, value: -1, to: mondayAfter)!
return HolidayDate(date: sunday, in: $0)
}
}

/// Calculates the date of new years day for a given year.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ final class CalculationContextReferenceTests: XCTestCase {
}
XCTAssertTrue(ctx.boolValue)
XCTAssertEqual(result, "Test")
ref.withContextVoid {
ref.withContext {
$0.boolValue = false
}
XCTAssertFalse(ctx.boolValue)
Expand Down
28 changes: 19 additions & 9 deletions Tests/SwiftyHolidaysTests/Gregorian/GregorianCalculatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import XCTest
@testable import SwiftyHolidays

final class GregorianCalculatorTests: XCTestCase {
private actor Promise<Value> {
var value: Value?

func fulfill(with value: Value) {
self.value = value
}
}

private let calculator = GregorianCalculator()

override func setUp() {
Expand Down Expand Up @@ -128,31 +136,33 @@ final class GregorianCalculatorTests: XCTestCase {
func testInitializing() {
_ = calculator.easterSunday(forYear: 2019)
let sema = DispatchSemaphore(value: 0)
calculator.contextRef.withContextVoid { $0.semaphores[2019, default: [:]][.easterSunday] = sema }
calculator.contextRef.withContext { $0.semaphores[2019, default: [:]][.easterSunday] = sema }
calculator.initialize(with: .init())
XCTAssertTrue(calculator.context.storage.isEmpty)
XCTAssertEqual(sema.wait(timeout: .now()), .success)
}

func testAwaitingCalculation() {
func testAwaitingCalculation() async {
let sema = DispatchSemaphore(value: 0)
let date = HolidayDate(day: 21, month: 4, year: 2019)
calculator.contextRef.withContextVoid { $0.semaphores[date.year, default: [:]][.easterSunday] = sema }
calculator.contextRef.withContext { $0.semaphores[date.year, default: [:]][.easterSunday] = sema }
let awaitExpectation = expectation(description: "Waiting for the calculator to wait for the semaphore")
awaitExpectation.isInverted = true
DispatchQueue.global().async {
_ = self.calculator.easterSunday(forYear: date.year)
awaitExpectation.fulfill()
}
wait(for: [awaitExpectation], timeout: 2)
calculator.contextRef.withContextVoid { $0.fulfill(.easterSunday, with: date) }
await fulfillment(of: [awaitExpectation], timeout: 2)
calculator.contextRef.withContext { $0.fulfill(.easterSunday, with: date) }
let calcExpectation = expectation(description: "Waiting for the calculator to return the calculated result")
var result: HolidayDate?
DispatchQueue.global().async {
result = self.calculator.easterSunday(forYear: date.year)
let resultPromise = Promise<HolidayDate>()
Task.detached {
let calculated = self.calculator.easterSunday(forYear: date.year)
await resultPromise.fulfill(with: calculated)
calcExpectation.fulfill()
}
wait(for: [calcExpectation], timeout: 2)
await fulfillment(of: [calcExpectation], timeout: 2)
let result = await resultPromise.value
XCTAssertEqual(result, date)
}

Expand Down

0 comments on commit 00067a9

Please sign in to comment.