12276: Reduce number of spurious invalidations sent to kernel.
[arvados.git] / services / fuse / arvados_fuse / fusefile.py
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 import json
6 import llfuse
7 import logging
8 import re
9 import time
10
11 from fresh import FreshBase, convertTime
12
13 _logger = logging.getLogger('arvados.arvados_fuse')
14
15 class File(FreshBase):
16     """Base for file objects."""
17
18     def __init__(self, parent_inode, _mtime=0):
19         super(File, self).__init__()
20         self.inode = None
21         self.parent_inode = parent_inode
22         self._mtime = _mtime
23
24     def size(self):
25         return 0
26
27     def readfrom(self, off, size, num_retries=0):
28         return ''
29
30     def writeto(self, off, size, num_retries=0):
31         raise Exception("Not writable")
32
33     def mtime(self):
34         return self._mtime
35
36     def clear(self):
37         pass
38
39     def writable(self):
40         return False
41
42     def flush(self):
43         pass
44
45
46 class FuseArvadosFile(File):
47     """Wraps a ArvadosFile."""
48
49     def __init__(self, parent_inode, arvfile, _mtime):
50         super(FuseArvadosFile, self).__init__(parent_inode, _mtime)
51         self.arvfile = arvfile
52
53     def size(self):
54         with llfuse.lock_released:
55             return self.arvfile.size()
56
57     def readfrom(self, off, size, num_retries=0):
58         with llfuse.lock_released:
59             return self.arvfile.readfrom(off, size, num_retries, exact=True)
60
61     def writeto(self, off, buf, num_retries=0):
62         with llfuse.lock_released:
63             return self.arvfile.writeto(off, buf, num_retries)
64
65     def stale(self):
66         return False
67
68     def writable(self):
69         return self.arvfile.writable()
70
71     def flush(self):
72         with llfuse.lock_released:
73             if self.writable():
74                 self.arvfile.parent.root_collection().save()
75
76
77 class StringFile(File):
78     """Wrap a simple string as a file"""
79     def __init__(self, parent_inode, contents, _mtime):
80         super(StringFile, self).__init__(parent_inode, _mtime)
81         self.contents = contents
82
83     def size(self):
84         return len(self.contents)
85
86     def readfrom(self, off, size, num_retries=0):
87         return self.contents[off:(off+size)]
88
89
90 class ObjectFile(StringFile):
91     """Wrap a dict as a serialized json object."""
92
93     def __init__(self, parent_inode, obj):
94         super(ObjectFile, self).__init__(parent_inode, "", 0)
95         self.object_uuid = obj['uuid']
96         self.update(obj)
97
98     def uuid(self):
99         return self.object_uuid
100
101     def update(self, obj=None):
102         if obj is None:
103             # TODO: retrieve the current record for self.object_uuid
104             # from the server. For now, at least don't crash when
105             # someone tells us it's a good time to update but doesn't
106             # pass us a fresh obj. See #8345
107             return
108         self._mtime = convertTime(obj['modified_at']) if 'modified_at' in obj else 0
109         self.contents = json.dumps(obj, indent=4, sort_keys=True) + "\n"
110
111     def persisted(self):
112         return True
113
114
115 class FuncToJSONFile(StringFile):
116     """File content is the return value of a given function, encoded as JSON.
117
118     The function is called at the time the file is read. The result is
119     cached until invalidate() is called.
120     """
121     def __init__(self, parent_inode, func):
122         super(FuncToJSONFile, self).__init__(parent_inode, "", 0)
123         self.func = func
124
125         # invalidate_inode() is asynchronous with no callback to wait for. In
126         # order to guarantee userspace programs don't get stale data that was
127         # generated before the last invalidate(), we must disallow inode
128         # caching entirely.
129         self.allow_attr_cache = False
130
131     def size(self):
132         self._update()
133         return super(FuncToJSONFile, self).size()
134
135     def readfrom(self, *args, **kwargs):
136         self._update()
137         return super(FuncToJSONFile, self).readfrom(*args, **kwargs)
138
139     def _update(self):
140         if not self.stale():
141             return
142         self._mtime = time.time()
143         obj = self.func()
144         self.contents = json.dumps(obj, indent=4, sort_keys=True) + "\n"
145         self.fresh()