• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pantsbuild / pants / 20332790708

18 Dec 2025 09:48AM UTC coverage: 64.992% (-15.3%) from 80.295%
20332790708

Pull #22949

github

web-flow
Merge f730a56cd into 407284c67
Pull Request #22949: Add experimental uv resolver for Python lockfiles

54 of 97 new or added lines in 5 files covered. (55.67%)

8270 existing lines in 295 files now uncovered.

48990 of 75379 relevant lines covered (64.99%)

1.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.07
/src/python/pants/pantsd/service/pants_service.py
1
# Copyright 2015 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
import logging
5✔
5
import threading
5✔
6
import time
5✔
7
from abc import ABC, abstractmethod
5✔
8
from collections.abc import KeysView
5✔
9
from dataclasses import dataclass
5✔
10

11
logger = logging.getLogger(__name__)
5✔
12

13

14
class PantsService(ABC):
5✔
15
    """Pants daemon service base class.
16

17
    The service lifecycle is made up of states described in the _ServiceState class, and controlled
18
    by a calling thread that is holding the Service `lifecycle_lock`. Under that lock, a caller
19
    can signal a service to "pause", "run", or "terminate" (see _ServiceState for more details).
20

21
    pantsd pauses all Services before forking a pantsd in order to ensure that no "relevant"
22
    locks are held (or awaited: see #6565) by threads that might not survive the fork. While paused,
23
    a Service must not have any threads running that might interact with any non-private locks.
24

25
    After forking, the pantsd (child) process should call `terminate()` to finish shutting down
26
    the service, and the parent process should call `resume()` to cause the service to resume running.
27
    """
28

29
    class ServiceError(Exception):
5✔
30
        pass
5✔
31

32
    def __init__(self):
5✔
33
        super().__init__()
1✔
34
        self.name = self.__class__.__name__
1✔
35
        self._state = _ServiceState()
1✔
36

37
    def setup(self, services: tuple["PantsService", ...]):
5✔
38
        """Called before `run` to allow for service->service or other side-effecting setup."""
39
        self.services = services
1✔
40

41
    @abstractmethod
5✔
42
    def run(self):
5✔
43
        """The main entry-point for the service called by the service runner."""
44

45
    def mark_pausing(self):
5✔
46
        """Triggers pausing of the service, without waiting for it to have paused.
47

48
        See the class and _ServiceState pydocs.
49
        """
UNCOV
50
        self._state.mark_pausing()
×
51

52
    def await_paused(self):
5✔
53
        """Once a service has been marked pausing, waits for it to have paused.
54

55
        See the class and _ServiceState pydocs.
56
        """
57
        self._state.await_paused()
×
58

59
    def resume(self):
5✔
60
        """Triggers the service to resume running, without waiting.
61

62
        See the class and _ServiceState pydocs.
63
        """
UNCOV
64
        self._state.mark_running()
×
65

66
    def terminate(self):
5✔
67
        """Triggers termination of the service, without waiting.
68

69
        See the class and _ServiceState pydocs.
70
        """
71
        self._state.mark_terminating()
1✔
72

73

74
class _ServiceState:
5✔
75
    """A threadsafe state machine for controlling a service running in another thread.
76

77
    The state machine represents two stable states:
78
      Running
79
      Paused
80
    And two transitional states:
81
      Pausing
82
      Terminating
83

84
    The methods of this class allow a caller to ask the Service to transition states, and then wait
85
    for those transitions to occur.
86

87
    A simplifying assumption is that there is one service thread that interacts with the state, and
88
    only one controlling thread. In the case of `pantsd`, the "one calling thread" condition is
89
    protected by the service `lifecycle_lock`.
90

91
    A complicating assumption is that while a service thread is `Paused`, it must be in a position
92
    where it could safely disappear and never come back. This is accounted for by having the service
93
    thread wait on a Condition variable while Paused: testing indicates that for multiple Pythons
94
    on both OSX and Linux, this does not result in poisoning of the associated Lock.
95
    """
96

97
    _RUNNING = "Running"
5✔
98
    _PAUSED = "Paused"
5✔
99
    _PAUSING = "Pausing"
5✔
100
    _TERMINATING = "Terminating"
5✔
101

102
    def __init__(self):
5✔
103
        """Creates a ServiceState in the Running state."""
104
        self._state = self._RUNNING
1✔
105
        self._lock = threading.Lock()
1✔
106
        self._condition = threading.Condition(self._lock)
1✔
107

108
    def _set_state(self, state, *valid_states):
5✔
109
        if valid_states and self._state not in valid_states:
1✔
110
            raise AssertionError(f"Cannot move {self} to `{state}` while it is `{self._state}`.")
×
111
        self._state = state
1✔
112
        self._condition.notify_all()
1✔
113

114
    def await_paused(self, timeout=None):
5✔
115
        """Blocks until the service is in the Paused state, then returns True.
116

117
        If a timeout is specified, the method may return False to indicate a timeout: with no timeout
118
        it will always (eventually) return True.
119

120
        Raises if the service is not currently in the Pausing state.
121
        """
UNCOV
122
        deadline = time.time() + timeout if timeout else None
×
UNCOV
123
        with self._lock:
×
124
            # Wait until the service transitions out of Pausing.
UNCOV
125
            while self._state != self._PAUSED:
×
UNCOV
126
                if self._state != self._PAUSING:
×
127
                    raise AssertionError(
×
128
                        f"Cannot wait for {self} to reach `{self._PAUSED}` while it is in `{self._state}`."
129
                    )
UNCOV
130
                timeout = deadline - time.time() if deadline else None
×
UNCOV
131
                if timeout and timeout <= 0:
×
UNCOV
132
                    return False
×
UNCOV
133
                self._condition.wait(timeout=timeout)
×
UNCOV
134
            return True
×
135

136
    def maybe_pause(self, timeout=None):
5✔
137
        """Called by the service to indicate that it is pausable.
138

139
        If the service calls this method while the state is `Pausing`, the state will transition
140
        to `Paused`, and the service will block here until it is marked `Running` or `Terminating`.
141

142
        If the state is not currently `Pausing`, and a timeout is not passed, this method returns
143
        immediately. If a timeout is passed, this method blocks up to that number of seconds to wait
144
        to transition to `Pausing`.
145
        """
146
        deadline = time.time() + timeout if timeout else None
1✔
147
        with self._lock:
1✔
148
            while self._state != self._PAUSING:
1✔
149
                # If we've been terminated, or the deadline has passed, return.
150
                timeout = deadline - time.time() if deadline else None
1✔
151
                if self._state == self._TERMINATING or not timeout or timeout <= 0:
1✔
152
                    return
1✔
153
                # Otherwise, wait for the state to change.
154
                self._condition.wait(timeout=timeout)
1✔
155

156
            # Set Paused, and then wait until we are no longer Paused.
UNCOV
157
            self._set_state(self._PAUSED, self._PAUSING)
×
UNCOV
158
            while self._state == self._PAUSED:
×
UNCOV
159
                self._condition.wait()
×
160

161
    def mark_pausing(self):
5✔
162
        """Requests that the service move to the Paused state, without waiting for it to do so.
163

164
        Raises if the service is not currently in the Running state.
165
        """
UNCOV
166
        with self._lock:
×
UNCOV
167
            self._set_state(self._PAUSING, self._RUNNING)
×
168

169
    def mark_running(self):
5✔
170
        """Moves the service to the Running state.
171

172
        Raises if the service is not currently in the Paused state.
173
        """
UNCOV
174
        with self._lock:
×
UNCOV
175
            self._set_state(self._RUNNING, self._PAUSED)
×
176

177
    def mark_terminating(self):
5✔
178
        """Requests that the service move to the Terminating state, without waiting for it to do
179
        so."""
180
        with self._lock:
1✔
181
            self._set_state(self._TERMINATING)
1✔
182

183
    @property
5✔
184
    def is_terminating(self):
5✔
185
        """Returns True if the Service should currently be terminating.
186

187
        NB: `Terminating` does not have an associated "terminated" state, because the caller uses
188
        liveness of the service thread to determine when a service is terminated.
189
        """
190
        with self._lock:
1✔
191
            return self._state == self._TERMINATING
1✔
192

193

194
@dataclass(frozen=True)
5✔
195
class PantsServices:
5✔
196
    """A collection of running PantsServices threads."""
197

198
    JOIN_TIMEOUT_SECONDS = 1
5✔
199

200
    _service_threads: dict[PantsService, threading.Thread]
5✔
201

202
    def __init__(self, services: tuple[PantsService, ...] = ()) -> None:
5✔
203
        object.__setattr__(self, "_service_threads", self._start(services))
1✔
204

205
    @classmethod
5✔
206
    def _make_thread(cls, service):
5✔
207
        name = f"{service.__class__.__name__}Thread"
×
208
        t = threading.Thread(target=service.run, name=name)
×
209
        t.daemon = True
×
210
        return t
×
211

212
    @classmethod
5✔
213
    def _start(cls, services: tuple[PantsService, ...]) -> dict[PantsService, threading.Thread]:
5✔
214
        """Launch a thread per service."""
215

216
        for service in services:
1✔
217
            logger.debug(f"setting up service {service}")
×
218
            service.setup(services)
×
219

220
        service_thread_map = {service: cls._make_thread(service) for service in services}
1✔
221

222
        for service, service_thread in service_thread_map.items():
1✔
223
            logger.debug(f"starting service {service}")
×
224
            service_thread.start()
×
225

226
        return service_thread_map
1✔
227

228
    @property
5✔
229
    def services(self) -> KeysView[PantsService]:
5✔
230
        return self._service_threads.keys()
×
231

232
    def are_all_alive(self) -> bool:
5✔
233
        """Return true if all services threads are still alive, and false if any have died.
234

235
        This method does not have side effects: if one service thread has died, the rest should be
236
        killed and joined via `self.shutdown()`.
237
        """
238
        for service, service_thread in self._service_threads.items():
×
239
            if not service_thread.is_alive():
×
240
                logger.error(f"service failure for {service}.")
×
241
                return False
×
242
        return True
×
243

244
    def shutdown(self) -> None:
5✔
245
        """Shut down and join all service threads."""
246
        for service, service_thread in self._service_threads.items():
1✔
247
            service.terminate()
×
248
        for service, service_thread in self._service_threads.items():
1✔
249
            logger.debug(f"terminating pantsd service: {service}")
×
250
            service_thread.join(self.JOIN_TIMEOUT_SECONDS)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc