Skip to content

Commit 3d7a2ad

Browse files
authored
Implement file locking for Nexrad Level2 and IRIS/Sigmet backends (#269)
* introduce file lock for nexrad level2 backend * add regression test * add history.md entry * add iris file lock * fix history.md
1 parent fc43e60 commit 3d7a2ad

File tree

5 files changed

+95
-41
lines changed

5 files changed

+95
-41
lines changed

docs/history.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* FIX: Correct retrieval of intermediate records in nexrad level2 reader ({issue}`259`) ({pull}`261`) by [@kmuehlbauer](https://github.com/kmuehlbauer).
1313
* FIX: Test for magic number BZhX1AY&SY (where X is any number between 0..9) when retrieving BZ2 record indices in nexrad level2 reader ({issue}`264`) ({pull}`266`) by [@kmuehlbauer](https://github.com/kmuehlbauer).
1414
* ENH: Add message type 1 decoding to nexrad level 2 reader ({issue}`256`) ({pull}`267`) by [@kmuehlbauer](https://github.com/kmuehlbauer).
15+
* ENH: Introduce file locks for nexrad level2 and iris backend ({issue}`207`) ({pull}`268`) by [@kmuehlbauer](https://github.com/kmuehlbauer).
1516

1617
## 0.8.0 (2024-11-04)
1718

tests/__init__.py

+14
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,17 @@
33
# Distributed under the MIT License. See LICENSE for more info.
44

55
"""Unit test package for xradar."""
6+
7+
import importlib
8+
9+
import pytest
10+
11+
12+
def skip_import(name):
13+
try:
14+
importlib.import_module(name)
15+
found = True
16+
except ImportError:
17+
found = False
18+
19+
return pytest.mark.skipif(not found, reason=f"requires {name}")

tests/io/test_io.py

+18
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import xarray as xr
1515

1616
import xradar.io
17+
from tests import skip_import
1718
from xradar.io import (
1819
open_cfradial1_datatree,
1920
open_datamet_datatree,
@@ -1101,3 +1102,20 @@ def test_open_nexradlevel2_datatree(nexradlevel2_files):
11011102
}
11021103
assert np.round(ds.elevation.mean().values.item(), 1) == elevations[i]
11031104
assert ds.sweep_number.values == int(grp[6:])
1105+
1106+
1107+
@skip_import("dask")
1108+
@pytest.mark.parametrize(
1109+
"nexradlevel2_files", ["nexradlevel2_gzfile", "nexradlevel2_bzfile"], indirect=True
1110+
)
1111+
def test_nexradlevel2_dask_load(nexradlevel2_files):
1112+
ds = xr.open_dataset(nexradlevel2_files, group="sweep_0", engine="nexradlevel2")
1113+
dsc = ds.chunk()
1114+
dsc.load()
1115+
1116+
1117+
@skip_import("dask")
1118+
def test_iris_dask_load(iris0_file):
1119+
ds = xr.open_dataset(iris0_file, group="sweep_0", engine="iris")
1120+
dsc = ds.chunk()
1121+
dsc.load()

xradar/io/backends/iris.py

+16-6
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from xarray import DataTree
4747
from xarray.backends.common import AbstractDataStore, BackendArray, BackendEntrypoint
4848
from xarray.backends.file_manager import CachingFileManager
49+
from xarray.backends.locks import SerializableLock, ensure_lock
4950
from xarray.backends.store import StoreBackendEntrypoint
5051
from xarray.core import indexing
5152
from xarray.core.utils import FrozenDict
@@ -72,6 +73,9 @@
7273
_get_subgroup,
7374
)
7475

76+
IRIS_LOCK = SerializableLock()
77+
78+
7579
#: mapping from IRIS names to CfRadial2/ODIM
7680
iris_mapping = {
7781
"DB_DBT": "DBTH",
@@ -3786,9 +3790,10 @@ def __init__(self, datastore, name, var):
37863790
self.shape = (nrays, nbins)
37873791

37883792
def _getitem(self, key):
3789-
# read the data and put it into dict
3790-
self.datastore.root.get_moment(self.group, self.name)
3791-
return self.datastore.ds["sweep_data"][self.name][key]
3793+
with self.datastore.lock:
3794+
# read the data and put it into dict
3795+
self.datastore.root.get_moment(self.group, self.name)
3796+
return self.datastore.ds["sweep_data"][self.name][key]
37923797

37933798
def __getitem__(self, key):
37943799
return indexing.explicit_indexing_adapter(
@@ -3805,16 +3810,19 @@ class IrisStore(AbstractDataStore):
38053810
Ported from wradlib.
38063811
"""
38073812

3808-
def __init__(self, manager, group=None):
3813+
def __init__(self, manager, group=None, lock=IRIS_LOCK):
38093814
self._manager = manager
38103815
self._group = int(group[6:]) + 1
38113816
self._filename = self.filename
38123817
self._need_time_recalc = False
3818+
self.lock = ensure_lock(lock)
38133819

38143820
@classmethod
3815-
def open(cls, filename, mode="r", group=None, **kwargs):
3821+
def open(cls, filename, mode="r", group=None, lock=None, **kwargs):
3822+
if lock is None:
3823+
lock = IRIS_LOCK
38163824
manager = CachingFileManager(IrisRawFile, filename, mode=mode, kwargs=kwargs)
3817-
return cls(manager, group=group)
3825+
return cls(manager, group=group, lock=lock)
38183826

38193827
@property
38203828
def filename(self):
@@ -3991,6 +3999,7 @@ def open_dataset(
39913999
use_cftime=None,
39924000
decode_timedelta=None,
39934001
group=None,
4002+
lock=None,
39944003
first_dim="auto",
39954004
reindex_angle=False,
39964005
fix_second_angle=False,
@@ -4000,6 +4009,7 @@ def open_dataset(
40004009
store = IrisStore.open(
40014010
filename_or_obj,
40024011
group=group,
4012+
lock=lock,
40034013
loaddata=False,
40044014
)
40054015

xradar/io/backends/nexrad_level2.py

+46-35
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from xarray import DataTree
4545
from xarray.backends.common import AbstractDataStore, BackendArray, BackendEntrypoint
4646
from xarray.backends.file_manager import CachingFileManager
47+
from xarray.backends.locks import SerializableLock, ensure_lock
4748
from xarray.backends.store import StoreBackendEntrypoint
4849
from xarray.core import indexing
4950
from xarray.core.utils import FrozenDict, close_on_error
@@ -78,6 +79,9 @@
7879
string_dict,
7980
)
8081

82+
NEXRADL2_LOCK = SerializableLock()
83+
84+
8185
#: mapping from NEXRAD names to CfRadial2/ODIM
8286
nexrad_mapping = {
8387
"REF": "DBZH",
@@ -1324,36 +1328,32 @@ def __init__(self, datastore, name, var):
13241328
self.shape = (nrays, nbins)
13251329

13261330
def _getitem(self, key):
1327-
# read the data if not available
1328-
try:
1329-
data = self.datastore.ds["sweep_data"][self.name]["data"]
1330-
print("ZZZZZAA:", self.name, data, key)
1331-
except KeyError:
1332-
print("XXXXX:", self.group, self.name)
1333-
self.datastore.root.get_data(self.group, self.name)
1334-
data = self.datastore.ds["sweep_data"][self.name]["data"]
1335-
print("ZZZZZBB:", self.name, data, key)
1336-
print("YY0:", self.name, len(data), len(data[0]))
1337-
# see 3.2.4.17.6 Table XVII-I Data Moment Characteristics and Conversion for Data Names
1338-
word_size = self.datastore.ds["sweep_data"][self.name]["word_size"]
1339-
if self.name == "PHI" and word_size == 16:
1340-
# 10 bit mask, but only for 2 byte data
1341-
x = np.uint16(0x3FF)
1342-
elif self.name == "ZDR" and word_size == 16:
1343-
# 11 bit mask, but only for 2 byte data
1344-
x = np.uint16(0x7FF)
1345-
else:
1346-
x = np.uint8(0xFF)
1347-
print("YY1:", self.name, len(data[0]), self.shape)
1348-
if len(data[0]) < self.shape[1]:
1349-
return np.pad(
1350-
np.vstack(data) & x,
1351-
((0, 0), (0, self.shape[1] - len(data[0]))),
1352-
mode="constant",
1353-
constant_values=0,
1354-
)[key]
1355-
else:
1356-
return (np.vstack(data) & x)[key]
1331+
with self.datastore.lock:
1332+
# read the data if not available
1333+
try:
1334+
data = self.datastore.ds["sweep_data"][self.name]["data"]
1335+
except KeyError:
1336+
self.datastore.root.get_data(self.group, self.name)
1337+
data = self.datastore.ds["sweep_data"][self.name]["data"]
1338+
# see 3.2.4.17.6 Table XVII-I Data Moment Characteristics and Conversion for Data Names
1339+
word_size = self.datastore.ds["sweep_data"][self.name]["word_size"]
1340+
if self.name == "PHI" and word_size == 16:
1341+
# 10 bit mask, but only for 2 byte data
1342+
x = np.uint16(0x3FF)
1343+
elif self.name == "ZDR" and word_size == 16:
1344+
# 11 bit mask, but only for 2 byte data
1345+
x = np.uint16(0x7FF)
1346+
else:
1347+
x = np.uint8(0xFF)
1348+
if len(data[0]) < self.shape[1]:
1349+
return np.pad(
1350+
np.vstack(data) & x,
1351+
((0, 0), (0, self.shape[1] - len(data[0]))),
1352+
mode="constant",
1353+
constant_values=0,
1354+
)[key]
1355+
else:
1356+
return (np.vstack(data) & x)[key]
13571357

13581358
def __getitem__(self, key):
13591359
return indexing.explicit_indexing_adapter(
@@ -1365,24 +1365,29 @@ def __getitem__(self, key):
13651365

13661366

13671367
class NexradLevel2Store(AbstractDataStore):
1368-
def __init__(self, manager, group=None):
1368+
def __init__(self, manager, group=None, lock=NEXRADL2_LOCK):
13691369
self._manager = manager
13701370
self._group = int(group[6:])
13711371
self._filename = self.filename
1372+
self.lock = ensure_lock(lock)
13721373

13731374
@classmethod
1374-
def open(cls, filename, mode="r", group=None, **kwargs):
1375+
def open(cls, filename, mode="r", group=None, lock=None, **kwargs):
1376+
if lock is None:
1377+
lock = NEXRADL2_LOCK
13751378
manager = CachingFileManager(
13761379
NEXRADLevel2File, filename, mode=mode, kwargs=kwargs
13771380
)
1378-
return cls(manager, group=group)
1381+
return cls(manager, group=group, lock=lock)
13791382

13801383
@classmethod
1381-
def open_groups(cls, filename, groups, mode="r", **kwargs):
1384+
def open_groups(cls, filename, groups, mode="r", lock=None, **kwargs):
1385+
if lock is None:
1386+
lock = NEXRADL2_LOCK
13821387
manager = CachingFileManager(
13831388
NEXRADLevel2File, filename, mode=mode, kwargs=kwargs
13841389
)
1385-
return {group: cls(manager, group=group) for group in groups}
1390+
return {group: cls(manager, group=group, lock=lock) for group in groups}
13861391

13871392
@property
13881393
def filename(self):
@@ -1534,6 +1539,7 @@ def open_dataset(
15341539
use_cftime=None,
15351540
decode_timedelta=None,
15361541
group=None,
1542+
lock=None,
15371543
first_dim="auto",
15381544
reindex_angle=False,
15391545
fix_second_angle=False,
@@ -1543,6 +1549,7 @@ def open_dataset(
15431549
store = NexradLevel2Store.open(
15441550
filename_or_obj,
15451551
group=group,
1552+
lock=lock,
15461553
loaddata=False,
15471554
)
15481555

@@ -1610,6 +1617,7 @@ def open_nexradlevel2_datatree(
16101617
fix_second_angle=False,
16111618
site_coords=True,
16121619
optional=True,
1620+
lock=None,
16131621
**kwargs,
16141622
):
16151623
"""Open a NEXRAD Level2 dataset as an `xarray.DataTree`.
@@ -1732,6 +1740,7 @@ def open_nexradlevel2_datatree(
17321740
fix_second_angle=fix_second_angle,
17331741
site_coords=site_coords,
17341742
optional=optional,
1743+
lock=lock,
17351744
**kwargs,
17361745
)
17371746
ls_ds: list[xr.Dataset] = [sweep_dict[sweep] for sweep in sweep_dict.keys()]
@@ -1771,10 +1780,12 @@ def open_sweeps_as_dict(
17711780
fix_second_angle=False,
17721781
site_coords=True,
17731782
optional=True,
1783+
lock=None,
17741784
**kwargs,
17751785
):
17761786
stores = NexradLevel2Store.open_groups(
17771787
filename=filename_or_obj,
1788+
lock=lock,
17781789
groups=sweeps,
17791790
)
17801791
groups_dict = {}

0 commit comments

Comments
 (0)