Skip to content

Commit

Permalink
Merge pull request #11234 from bmerry/issue-11233
Browse files Browse the repository at this point in the history
Make leap-second initialisation thread-safe
  • Loading branch information
mhvk committed Jan 13, 2021
2 parents 7726650 + 282a1c7 commit aef31be
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 11 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,9 @@ astropy.tests
astropy.time
^^^^^^^^^^^^

- Fix a thread-safety issue with initialization of the leap-second table
(which is only an issue when ERFA's built-in table is out of date). [#11234]

astropy.timeseries
^^^^^^^^^^^^^^^^^^

Expand Down
39 changes: 29 additions & 10 deletions astropy/time/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

import os
import copy
import enum
import operator
import threading
from datetime import datetime, date, timedelta
from time import strftime
from warnings import warn
Expand Down Expand Up @@ -96,7 +98,14 @@
'IAU1994': {'function': erfa.gst94, 'scales': ('ut1',)}}}


_LEAP_SECONDS_CHECKED = False
class _LeapSecondsCheck(enum.Enum):
NOT_STARTED = 0 # No thread has reached the check
RUNNING = 1 # A thread is running update_leap_seconds (_LEAP_SECONDS_LOCK is held)
DONE = 2 # update_leap_seconds has completed


_LEAP_SECONDS_CHECK = _LeapSecondsCheck.NOT_STARTED
_LEAP_SECONDS_LOCK = threading.RLock()


class TimeInfo(MixinInfo):
Expand Down Expand Up @@ -1476,15 +1485,25 @@ def __new__(cls, val, val2=None, format=None, scale=None,
location=None, copy=False):

# Because of import problems, this can only be done on
# first call of Time.
global _LEAP_SECONDS_CHECKED
if not _LEAP_SECONDS_CHECKED:
# *Must* set to True first as update_leap_seconds uses Time.
# In principle, this may cause wrong leap seconds in
# update_leap_seconds itself, but since expiration is in
# units of days, that is fine.
_LEAP_SECONDS_CHECKED = True
update_leap_seconds()
# first call of Time. The initialization is complicated because
# update_leap_seconds uses Time.
# In principle, this may cause wrong leap seconds in
# update_leap_seconds itself, but since expiration is in
# units of days, that is fine.
global _LEAP_SECONDS_CHECK
if _LEAP_SECONDS_CHECK != _LeapSecondsCheck.DONE:
with _LEAP_SECONDS_LOCK:
# There are three ways we can get here:
# 1. First call (NOT_STARTED).
# 2. Re-entrant call (RUNNING). We skip the initialisation
# and don't worry about leap second errors.
# 3. Another thread which raced with the first call
# (RUNNING). The first thread has relinquished the
# lock to us, so initialization is complete.
if _LEAP_SECONDS_CHECK == _LeapSecondsCheck.NOT_STARTED:
_LEAP_SECONDS_CHECK = _LeapSecondsCheck.RUNNING
update_leap_seconds()
_LEAP_SECONDS_CHECK = _LeapSecondsCheck.DONE

if isinstance(val, Time):
self = val.replicate(format=format, copy=copy, cls=cls)
Expand Down
18 changes: 17 additions & 1 deletion astropy/time/tests/test_update_leap_seconds.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta

import pytest
Expand All @@ -7,7 +8,8 @@
from astropy.utils import iers
from astropy.utils.exceptions import AstropyWarning

from astropy.time import update_leap_seconds
import astropy.time.core
from astropy.time import update_leap_seconds, Time


class TestUpdateLeapSeconds:
Expand Down Expand Up @@ -81,3 +83,17 @@ def test_auto_update_expired_file(self, tmpdir):

with pytest.warns(iers.IERSStaleWarning):
update_leap_seconds(['erfa', expired_file])

def test_init_thread_safety(self, monkeypatch):
# Set up expired ERFA leap seconds.
expired = self.erfa_ls[self.erfa_ls['year'] < 2017]
expired.update_erfa_leap_seconds(initialize_erfa='empty')
# Force re-initialization, even if another test already did it
monkeypatch.setattr(astropy.time.core, '_LEAP_SECONDS_CHECK',
astropy.time.core._LeapSecondsCheck.NOT_STARTED)
workers = 4
with ThreadPoolExecutor(max_workers=workers) as executor:
futures = [executor.submit(lambda: str(Time('2019-01-01 00:00:00.000').tai))
for i in range(workers)]
results = [future.result() for future in futures]
assert results == ['2019-01-01 00:00:37.000'] * workers

0 comments on commit aef31be

Please sign in to comment.