Skip to content

Commit dc9280b

Browse files
authored
Issue #536 - Remove pickle from config caching (#537)
1 parent 61001ed commit dc9280b

File tree

7 files changed

+197
-162
lines changed

7 files changed

+197
-162
lines changed

ait/core/dmc.py

+32-5
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@
2525
import datetime
2626
import math
2727
import os.path
28-
import pickle
2928
from typing import Tuple
3029

30+
import msgpack # type: ignore
3131
import requests
32+
from msgpack.exceptions import ExtraData # type: ignore
33+
from msgpack.exceptions import FormatError # type: ignore
34+
from msgpack.exceptions import StackError # type: ignore
3235

3336
import ait.core
3437
from ait.core import log
@@ -316,10 +319,22 @@ def _load_leap_second_data(self):
316319
try:
317320
log.info("Attempting to load leapseconds.dat")
318321
with open(ls_file, "rb") as outfile:
319-
self._data = pickle.load(outfile)
320-
log.info("Loaded leapseconds config file successfully")
322+
packed_data = outfile.read()
323+
324+
unpacked_data = msgpack.unpackb(packed_data, object_hook=mp_decode_datetime)
325+
326+
# msgpack converts tuples to lists, so have to convert back
327+
if unpacked_data and "leapseconds" in unpacked_data:
328+
lst_list = unpacked_data["leapseconds"]
329+
tup_list = [tuple(lst) for lst in lst_list]
330+
unpacked_data["leapseconds"] = tup_list
331+
self._data = unpacked_data
332+
log.info("Loaded leapseconds config file successfully")
333+
321334
except IOError:
322335
log.info("Unable to locate leapseconds config file")
336+
except (ValueError, ExtraData, FormatError, StackError):
337+
log.info("Unable to load leapseconds.dat")
323338

324339
if not (self._data and self.is_valid()):
325340
try:
@@ -391,11 +406,23 @@ def _update_leap_second_data(self):
391406
log.info("Leapsecond data processed")
392407

393408
self._data = data
409+
packed_data = msgpack.packb(data, default=mp_encode_datetime)
394410
with open(ls_file, "wb") as outfile:
395-
pickle.dump(data, outfile)
396-
411+
outfile.write(packed_data)
397412
log.info("Successfully generated leapseconds config file")
398413

399414

415+
def mp_decode_datetime(obj):
416+
if "__datetime__" in obj:
417+
obj = datetime.datetime.strptime(obj["as_str"], "%Y%m%dT%H:%M:%S.%f")
418+
return obj
419+
420+
421+
def mp_encode_datetime(obj):
422+
if isinstance(obj, datetime.datetime):
423+
return {"__datetime__": True, "as_str": obj.strftime("%Y%m%dT%H:%M:%S.%f")}
424+
return obj
425+
426+
400427
if not LeapSeconds:
401428
LeapSeconds = UTCLeapSeconds()

ait/core/table.py

+1-19
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
import datetime
1515
import hashlib
1616
import io
17-
import os
18-
import pickle
1917

2018
import yaml
2119

@@ -457,27 +455,11 @@ def __init__(self, filename=None):
457455
filename = ait.config.get("table.filename")
458456

459457
self.filename = filename
460-
self.cachename = os.path.splitext(filename)[0] + ".pkl"
461458
self.fswtabdict = None
462459

463-
@property
464-
def dirty(self):
465-
"""True if the pickle cache needs to be regenerated, False to
466-
use current pickle binary"""
467-
return util.check_yaml_timestamps(self.filename, self.cachename)
468-
469460
def load(self):
470461
if self.fswtabdict is None:
471-
if self.dirty:
472-
self.fswtabdict = FSWTabDict(self.filename)
473-
util.update_cache(self.filename, self.cachename, self.fswtabdict)
474-
log.info(f"Loaded new pickle file: {self.cachename}")
475-
else:
476-
with open(self.cachename, "rb") as stream:
477-
self.fswtabdict = pickle.load(stream)
478-
log.info(
479-
"Current pickle file loaded: " f'{self.cachename.split("/")[-1]}'
480-
)
462+
self.fswtabdict = FSWTabDict(self.filename)
481463

482464
return self.fswtabdict
483465

ait/core/tlm.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -744,7 +744,7 @@ class PacketExpression:
744744
"""PacketExpression
745745
746746
A Packet Expression is a simple mathematical expression that can
747-
be evaluted in the context of a Packet. Names in the formula
747+
be evaluated in the context of a Packet. Names in the formula
748748
refer to fields in the packet.
749749
750750
Packet Expressions provide a convenient mechanism to express and

ait/core/util.py

+29-74
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#!/usr/bin/env python2.7
21
# Advanced Multi-Mission Operations System (AMMOS) Instrument Toolkit (AIT)
32
# Bespoke Link to Instruments and Small Satellites (BLISS)
43
#
@@ -36,30 +35,14 @@ def __init__(self, filename, loader):
3635
"""
3736
Creates a new ObjectCache
3837
39-
Caches the Python object returned by loader(filename), using
40-
Python's pickle object serialization mechanism. An ObjectCache
41-
is useful when loader(filename) is slow.
38+
Caches the Python object returned by loader(filename).
39+
An ObjectCache is useful when loader(filename) is slow.
4240
43-
The result of loader(filename) is cached to cachename, the
44-
basename of filename with a '.pkl' extension.
45-
46-
Use the load() method to load, either via loader(filename) or
47-
the pickled cache file, whichever was modified most recently.
41+
Use the load() method to load
4842
"""
4943
self._loader = loader
5044
self._dict = None
5145
self._filename = filename
52-
self._cachename = os.path.splitext(filename)[0] + ".pkl"
53-
54-
@property
55-
def cachename(self):
56-
"""The pickled cache filename"""
57-
return self._cachename
58-
59-
@property
60-
def dirty(self):
61-
"""True if the pickle cache needs to be regenerated, False to use current pickle binary"""
62-
return check_yaml_timestamps(self.filename, self.cachename)
6346

6447
@property
6548
def filename(self):
@@ -70,19 +53,11 @@ def load(self):
7053
"""
7154
Loads the Python object
7255
73-
Loads the Python object, either via loader (filename) or the
74-
pickled cache file, whichever was modified most recently.
56+
Loads the Python object via loader (filename).
7557
"""
7658

7759
if self._dict is None:
78-
if self.dirty:
79-
self._dict = self._loader(self.filename)
80-
update_cache(self.filename, self.cachename, self._dict)
81-
log.info(f"Loaded new pickle file: {self.cachename}")
82-
else:
83-
with open(self.cachename, "rb") as stream:
84-
self._dict = pickle.load(stream)
85-
log.info(f'Current pickle file loaded: {self.cachename.split("/")[-1]}')
60+
self._dict = self._loader(self.filename)
8661
return self._dict
8762

8863

@@ -94,45 +69,46 @@ def load(self):
9469
timer = time.time
9570

9671

97-
def check_yaml_timestamps(yaml_file_name, cache_name):
72+
def check_yaml_timestamps(yaml_file_name, cache_file_name):
9873
"""
99-
Checks YAML configuration file timestamp and any 'included' YAML configuration file's
100-
timestamp against the pickle cache file timestamp.
101-
The term 'dirty' means that a yaml config file has a more recent timestamp than the
102-
pickle cache file. If a pickle cache file is found to be 'dirty' (return true) the
103-
pickle cache file is not up-to-date, and a new pickle cache file must be generated.
104-
If the cache file in not 'dirty' (return false) the existing pickle binary will
105-
be loaded.
74+
Checks YAML configuration file timestamp and any 'included' YAML
75+
configuration file's timestamp against the cache file's timestamp.
76+
The term 'dirty' means that a yaml config file has a more recent
77+
timestamp than the cache file. If file is found to be 'dirty'
78+
(return True) the cache file can be considered not up-to-date.
79+
If the other file in not 'dirty' (return False) the cache file can be
80+
considered up-to-date.
10681
10782
param: yaml_file_name: str
10883
Name of the yaml configuration file to be tested
109-
param: cache_name: str
110-
Filename with path to the cached pickle file for this config file.
84+
param: cache_file_name: str
85+
Filename with path to the cache file to be compared
11186
11287
return: boolean
11388
True:
114-
Indicates 'dirty' pickle cache: i.e. the file is not current, generate new binary
89+
The cache file is not current, or does not exist
11590
False
116-
Load current cache file
91+
The cache file can be considered current
11792
11893
"""
119-
# If no pickle cache exists return True to make a new one.
120-
if not os.path.exists(cache_name):
121-
log.debug("No pickle cache exists, make a new one")
94+
# If no cache exists return True to make a new one.
95+
if not os.path.exists(cache_file_name):
96+
log.debug("No cache exists, make a new one")
12297
return True
123-
# Has the yaml config file has been modified since the creation of the pickle cache
124-
if os.path.getmtime(yaml_file_name) > os.path.getmtime(cache_name):
125-
log.info(f"{yaml_file_name} modified - make a new binary pickle cache file.")
98+
# Has the yaml config file has been modified since the creation of the cache
99+
if os.path.getmtime(yaml_file_name) > os.path.getmtime(cache_file_name):
100+
log.info(f"{yaml_file_name} modified - make a new cache file.")
126101
return True
127102
# Get the directory of the yaml config file to be parsed
128103
dir_name = os.path.dirname(yaml_file_name)
129-
# Open the yaml config file to look for '!includes' to be tested on the next iteration
104+
# Open the yaml config file to look for '!includes' to be tested
105+
# on the next iteration
130106
with open(yaml_file_name, "r") as file:
131107
try:
132108
for line in file:
133109
if not line.strip().startswith("#") and "!include" in line:
134110
check = check_yaml_timestamps(
135-
os.path.join(dir_name, line.strip().split(" ")[2]), cache_name
111+
os.path.join(dir_name, line.strip().split(" ")[2]), cache_file_name
136112
)
137113
if check:
138114
return True
@@ -144,27 +120,6 @@ def check_yaml_timestamps(yaml_file_name, cache_name):
144120
return False
145121

146122

147-
def update_cache(yaml_file_name, cache_file_name, object_to_serialize):
148-
"""
149-
Caches the result of loader (yaml_file_name) to pickle binary (cache_file_name), if
150-
the yaml config file has been modified since the last pickle cache was created, i.e.
151-
(the binary pickle cache is declared to be 'dirty' in 'check_yaml_timestamps()').
152-
153-
param: yaml_file_name: str
154-
Name of the yaml configuration file to be serialized ('pickled')
155-
param: cache_file_name: str
156-
File name with path to the new serialized cached pickle file for this config file.:
157-
param: object_to_serialize: object
158-
Object to serialize ('pickle') e.g. instance of 'ait.core.cmd.CmdDict'
159-
160-
"""
161-
162-
msg = f"Saving updates from more recent {yaml_file_name} to {cache_file_name}."
163-
log.info(msg)
164-
with open(cache_file_name, "wb") as output:
165-
pickle.dump(object_to_serialize, output, -1)
166-
167-
168123
def __init_extensions__(modname, modsyms): # noqa
169124
"""
170125
Initializes a module (given its name and :func:`globals()` symbol
@@ -290,11 +245,11 @@ def setDictDefaults(d, defaults): # noqa
290245

291246
def getDefaultDict(modname, config_key, loader, reload=False, filename=None): # noqa
292247
"""
293-
Returns default AIT dictonary for modname
248+
Returns default AIT dictionary for modname
294249
295250
This helper function encapulates the core logic necessary to
296-
(re)load, cache (via util.ObjectCache), and return the default
297-
dictionary. For example, in ait.core.cmd:
251+
(re)load and return the default dictionary.
252+
For example, in ait.core.cmd:
298253
299254
def getDefaultDict(reload=False):
300255
return ait.util.getDefaultDict(__name__, 'cmddict', CmdDict, reload)

config/leapseconds.dat

454 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)