Skip to content

Commit e6ee2b5

Browse files
committed
Add a locking mechanism when accessing CSV files.
This patch touches #435. Indeed, as discovered by @Yuki2718 and @DandelionSprout, our processes sometimes trigger a PermissionError exception on Windows. While testing with one of the latest builds of Windows 11 (Pro), I was able to reproduce the issue sometimes and therefore analyze it in more detail. It turns out that in some rare cases, 2 processes are simultaneously attempting to read the same CSV file. This patch attempts to solve the problem by introducing a shared lock which - for now - only affect Windows-Users. When running on Windows, all processes will share a lock that blocks when 2 processes try to access the same ressource at the same time. Contributors: * @Yuki2718 * @DandelionSprout
1 parent 985726d commit e6ee2b5

File tree

22 files changed

+197
-34
lines changed

22 files changed

+197
-34
lines changed

PyFunceble/checker/availability/base.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454

5555
import multiprocessing
5656
from datetime import datetime
57-
from typing import Dict, List, Optional
57+
from typing import Any, Dict, List, Optional
5858

5959
from sqlalchemy.orm import Session
6060

@@ -110,8 +110,12 @@ class AvailabilityCheckerBase(CheckerBase):
110110
Optional, Activates/Disables the check of the status before the actual
111111
status gathering.
112112
:param bool use_whois_db:
113-
Optional, Activates/Disable the usage of a local database to store the
113+
Optional, Activates/Disables the usage of a local database to store the
114114
WHOIS datasets.
115+
:param bool use_platform:
116+
Optional, Activates/Disables the usage of the platform features.
117+
:param multiprocessing.Lock shared_lock:
118+
Optional, The shared lock to use to access shared resources.
115119
"""
116120

117121
# pylint: disable=too-many-public-methods, too-many-instance-attributes
@@ -160,6 +164,7 @@ def __init__(
160164
db_session: Optional[Session] = None,
161165
use_whois_db: Optional[bool] = None,
162166
use_platform: Optional[bool] = None,
167+
shared_lock: Optional[Any] = None,
163168
) -> None:
164169
self.dns_query_tool = DNSQueryTool()
165170
self.whois_query_tool = WhoisQueryTool()
@@ -226,6 +231,7 @@ def __init__(
226231
do_syntax_check_first=do_syntax_check_first,
227232
db_session=db_session,
228233
use_platform=use_platform,
234+
shared_lock=shared_lock,
229235
)
230236

231237
@property
@@ -761,7 +767,9 @@ def try_to_query_status_from_whois(
761767
if (
762768
PyFunceble.facility.ConfigLoader.is_already_loaded() and self.use_whois_db
763769
): # pragma: no cover ## Not interesting enough to spend time on it.
764-
whois_object = get_whois_dataset_object(db_session=self.db_session)
770+
whois_object = get_whois_dataset_object(
771+
db_session=self.db_session, shared_lock=self.shared_lock
772+
)
765773
known_record = whois_object[self.subject]
766774

767775
if known_record and not isinstance(known_record, dict):

PyFunceble/checker/availability/domain.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ def try_to_query_status_from_reputation(self) -> "DomainAvailabilityChecker":
115115
self.status.idna_subject,
116116
)
117117

118-
lookup_result = DomainReputationChecker(self.status.idna_subject).get_status()
118+
lookup_result = DomainReputationChecker(
119+
self.status.idna_subject, shared_lock=self.shared_lock
120+
).get_status()
119121

120122
# pylint: disable=no-member
121123
if lookup_result and lookup_result.is_malicious():

PyFunceble/checker/availability/domain_and_ip.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ def query_status(
112112
db_session=self.db_session,
113113
use_whois_db=self.use_whois_db,
114114
use_platform=self.use_platform,
115+
shared_lock=self.shared_lock,
115116
)
116117
else:
117118
query_object = DomainAvailabilityChecker(
@@ -126,6 +127,7 @@ def query_status(
126127
db_session=self.db_session,
127128
use_whois_db=self.use_whois_db,
128129
use_platform=self.use_platform,
130+
shared_lock=self.shared_lock,
129131
)
130132

131133
query_object.dns_query_tool = self.dns_query_tool

PyFunceble/checker/base.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252

5353
import datetime
5454
import functools
55-
from typing import Optional
55+
from typing import Any, Optional
5656

5757
import domain2idna
5858
from sqlalchemy.orm import Session
@@ -77,6 +77,12 @@ class CheckerBase:
7777
7878
.. warning::
7979
This does not apply to the syntax checker - itself.
80+
:param Session db_session:
81+
Optional, The database session to use.
82+
:param bool use_platform:
83+
Optional, Authorizes the usage of the platform.
84+
:param Any shared_lock:
85+
Optional, The shared lock to use to access shared resources.
8086
"""
8187

8288
STD_DO_SYNTAX_CHECK_FIRST: bool = False
@@ -97,13 +103,16 @@ class CheckerBase:
97103
status: Optional[CheckerStatusBase] = None
98104
params: Optional[CheckerParamsBase] = None
99105

106+
shared_lock: Optional[Any] = None
107+
100108
def __init__(
101109
self,
102110
subject: Optional[str] = None,
103111
*,
104112
do_syntax_check_first: Optional[bool] = None,
105113
db_session: Optional[Session] = None,
106114
use_platform: Optional[bool] = None,
115+
shared_lock: Optional[Any] = None,
107116
) -> None:
108117
self.platform_query_tool = PlatformQueryTool()
109118
self.url2netloc = Url2Netloc()
@@ -115,6 +124,9 @@ def __init__(
115124
if self.status is None:
116125
self.status = CheckerStatusBase()
117126

127+
if shared_lock is not None:
128+
self.shared_lock = shared_lock
129+
118130
if subject is not None:
119131
self.subject = subject
120132

PyFunceble/checker/reputation/base.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
limitations under the License.
5151
"""
5252

53-
from typing import List, Optional
53+
from typing import Any, List, Optional
5454

5555
from sqlalchemy.orm import Session
5656

@@ -75,6 +75,12 @@ class ReputationCheckerBase(CheckerBase):
7575
:param bool do_syntax_check_first:
7676
Optional, Activates/Disables the check of the status before the actual
7777
status gathering.
78+
:param Session db_session:
79+
Optional, The database session to use.
80+
:param bool use_platform:
81+
Optional, Authorizes the usage of the platform.
82+
:param Any shared_lock:
83+
Optional, The shared lock to use to access shared resources.
7884
"""
7985

8086
dns_query_tool: Optional[DNSQueryTool] = None
@@ -92,9 +98,10 @@ def __init__(
9298
do_syntax_check_first: Optional[bool] = None,
9399
db_session: Optional[Session] = None,
94100
use_platform: Optional[bool] = None,
101+
shared_lock: Optional[Any] = None,
95102
) -> None:
96103
self.dns_query_tool = DNSQueryTool()
97-
self.ipv4_reputation_query_tool = IPV4ReputationDataset()
104+
self.ipv4_reputation_query_tool = IPV4ReputationDataset(shared_lock=shared_lock)
98105
self.domain_syntax_checker = DomainSyntaxChecker()
99106
self.ip_syntax_checker = IPSyntaxChecker()
100107
self.url_syntax_checker = URLSyntaxChecker()
@@ -110,6 +117,7 @@ def __init__(
110117
do_syntax_check_first=do_syntax_check_first,
111118
db_session=db_session,
112119
use_platform=use_platform,
120+
shared_lock=shared_lock,
113121
)
114122

115123
@staticmethod

PyFunceble/checker/syntax/base.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
limitations under the License.
5151
"""
5252

53-
from typing import Optional
53+
from typing import Any, Optional
5454

5555
from sqlalchemy.orm import Session
5656

@@ -78,13 +78,16 @@ def __init__(
7878
self,
7979
subject: Optional[str] = None,
8080
db_session: Optional[Session] = None,
81+
shared_lock: Optional[Any] = None,
8182
) -> None:
8283
self.params = SyntaxCheckerParams()
8384

8485
self.status = SyntaxCheckerStatus()
8586
self.status.params = self.params
8687

87-
super().__init__(subject=subject, db_session=db_session)
88+
super().__init__(
89+
subject=subject, db_session=db_session, shared_lock=shared_lock
90+
)
8891

8992
def subject_propagator(self) -> "CheckerBase":
9093
"""

PyFunceble/checker/utils/whois.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
limitations under the License.
5151
"""
5252

53-
from typing import Optional, Union
53+
from typing import Any, Optional, Union
5454

5555
from sqlalchemy.orm import Session
5656

@@ -64,13 +64,17 @@
6464

6565

6666
def get_whois_dataset_object(
67-
*, db_session: Optional[Session] = None
67+
*,
68+
db_session: Optional[Session] = None,
69+
shared_lock: Optional[Any] = None,
6870
) -> Union[DatasetBase, CSVDatasetBase, DBDatasetBase]:
6971
"""
7072
Provides the whois dataset object to work with.
7173
7274
:param db_session:
7375
A database session to use.
76+
:param shared_lock:
77+
Optional, The shared lock to use to access shared resources.
7478
7579
:raise ValueError:
7680
When the given database type is unknown.
@@ -80,7 +84,7 @@ def get_whois_dataset_object(
8084

8185
if PyFunceble.facility.ConfigLoader.is_already_loaded():
8286
if PyFunceble.storage.CONFIGURATION.cli_testing.db_type == "csv":
83-
return CSVWhoisDataset().set_authorized(
87+
return CSVWhoisDataset(shared_lock=shared_lock).set_authorized(
8488
bool(PyFunceble.storage.CONFIGURATION.cli_testing.whois_db)
8589
)
8690

PyFunceble/cli/processes/base.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,10 @@ class ProcessesManagerBase(PyFunceble.ext.process_manager.ProcessManagerCore):
6060
"""
6161
Provides the base of all classes.
6262
"""
63+
64+
def __init__(self, *args, **kwargs) -> None:
65+
# Better handling of shared locks.
66+
# This help be be retro-compatible. Allowing existing users to
67+
# not get exceptions.
68+
shared_lock = kwargs.pop("shared_lock", None)
69+
super().__init__(*args, **kwargs, shared_lock=shared_lock)

PyFunceble/cli/processes/workers/producer.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,15 @@ def perform_external_poweron_checks(self) -> None:
125125
self.file_printer = FilePrinter(
126126
skip_column=skip_columns, extra_formatters=extra_formatters
127127
)
128-
self.whois_dataset = get_whois_dataset_object(db_session=self.db_session)
129-
self.inactive_dataset = get_inactive_dataset_object(db_session=self.db_session)
130-
self.continue_dataset = get_continue_dataset_object(db_session=self.db_session)
128+
self.whois_dataset = get_whois_dataset_object(
129+
db_session=self.db_session, shared_lock=self.shared_lock
130+
)
131+
self.inactive_dataset = get_inactive_dataset_object(
132+
db_session=self.db_session, shared_lock=self.shared_lock
133+
)
134+
self.continue_dataset = get_continue_dataset_object(
135+
db_session=self.db_session, shared_lock=self.shared_lock
136+
)
131137
self.status_file_generator = StatusFileGenerator().guess_all_settings()
132138
self.counter = FilesystemCounter()
133139
self.registrar_counter = RegistrarCounter()

PyFunceble/cli/processes/workers/tester.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,12 @@ def perform_external_poweron_checks(self) -> None:
9090

9191
self.continue_dataset = (
9292
PyFunceble.cli.utils.testing.get_continue_dataset_object(
93-
db_session=self.db_session
93+
db_session=self.db_session, shared_lock=self.shared_lock
9494
)
9595
)
9696
self.inactive_dataset = (
9797
PyFunceble.cli.utils.testing.get_inactive_dataset_object(
98-
db_session=self.db_session
98+
db_session=self.db_session, shared_lock=self.shared_lock
9999
)
100100
)
101101

@@ -191,7 +191,7 @@ def _init_testing_object(
191191
self.initiated_testing_objects[checker_type][
192192
subject_type
193193
] = self.known_testing_objects[checker_type][subject_type](
194-
db_session=self.db_session
194+
db_session=self.db_session, shared_lock=self.shared_lock
195195
).set_do_syntax_check_first(
196196
# We want to always check the syntax first (ONLY UNDER THE CLI)
197197
not bool(PyFunceble.storage.CONFIGURATION.cli_testing.local_network)
@@ -278,12 +278,12 @@ def target(self, consumed: dict) -> Optional[Tuple[Any, ...]]:
278278
test_dataset["idna_subject"],
279279
)
280280

281-
self._init_testing_object(
281+
testing_object = self._init_testing_object(
282282
test_dataset["subject_type"], test_dataset["checker_type"]
283283
)
284284

285285
result = (
286-
self.testing_object.set_subject(test_dataset["idna_subject"])
286+
testing_object.set_subject(test_dataset["idna_subject"])
287287
.query_status()
288288
.get_status()
289289
)

0 commit comments

Comments
 (0)