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

pantsbuild / pants / 19015773527

02 Nov 2025 05:33PM UTC coverage: 17.872% (-62.4%) from 80.3%
19015773527

Pull #22816

github

web-flow
Merge a12d75757 into 6c024e162
Pull Request #22816: Update Pants internal Python to 3.14

4 of 5 new or added lines in 3 files covered. (80.0%)

28452 existing lines in 683 files now uncovered.

9831 of 55007 relevant lines covered (17.87%)

0.18 hits per line

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

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

4
from __future__ import annotations
1✔
5

6
import logging
1✔
7
import threading
1✔
8
from collections.abc import Iterator
1✔
9
from contextlib import contextmanager
1✔
10
from typing import Any, Protocol
1✔
11

12
from pants.build_graph.build_configuration import BuildConfiguration
1✔
13
from pants.engine.env_vars import CompleteEnvironmentVars
1✔
14
from pants.engine.internals.native_engine import PyExecutor
1✔
15
from pants.engine.unions import UnionMembership
1✔
16
from pants.init.engine_initializer import EngineInitializer, GraphScheduler
1✔
17
from pants.init.options_initializer import OptionsInitializer
1✔
18
from pants.option.bootstrap_options import AuthPluginResult, DynamicRemoteOptions
1✔
19
from pants.option.option_value_container import OptionValueContainer
1✔
20
from pants.option.options_bootstrapper import OptionsBootstrapper
1✔
21
from pants.option.options_diff import summarize_dynamic_options_diff, summarize_options_map_diff
1✔
22
from pants.option.options_fingerprinter import OptionsFingerprinter
1✔
23
from pants.option.scope import GLOBAL_SCOPE
1✔
24
from pants.pantsd.service.pants_service import PantsServices
1✔
25

26
logger = logging.getLogger(__name__)
1✔
27

28

29
class PantsServicesConstructor(Protocol):
1✔
30
    def __call__(
31
        self,
32
        bootstrap_options: OptionValueContainer,
33
        graph_scheduler: GraphScheduler,
34
    ) -> PantsServices: ...
35

36

37
class PantsDaemonCore:
1✔
38
    """A container for the state of a PantsDaemon that is affected by the bootstrap options.
39

40
    This class also serves to avoid a reference cycle between DaemonPantsRunner and PantsDaemon,
41
    which both have a reference to the core, and use it to get access to the Scheduler and current
42
    PantsServices.
43
    """
44

45
    def __init__(
1✔
46
        self,
47
        options_bootstrapper: OptionsBootstrapper,
48
        executor: PyExecutor,
49
        services_constructor: PantsServicesConstructor,
50
    ):
UNCOV
51
        self._options_initializer = OptionsInitializer(options_bootstrapper, executor)
×
UNCOV
52
        self._executor = executor
×
UNCOV
53
        self._services_constructor = services_constructor
×
UNCOV
54
        self._lifecycle_lock = threading.RLock()
×
55
        # N.B. This Event is used as nothing more than an atomic flag - nothing waits on it.
UNCOV
56
        self._kill_switch = threading.Event()
×
57

UNCOV
58
        self._scheduler: GraphScheduler | None = None
×
UNCOV
59
        self._services: PantsServices | None = None
×
60

UNCOV
61
        self._prior_options_map: dict[str, Any] | None = None
×
UNCOV
62
        self._prior_dynamic_remote_options: DynamicRemoteOptions | None = None
×
UNCOV
63
        self._prior_auth_plugin_result: AuthPluginResult | None = None
×
64

65
    def is_valid(self) -> bool:
1✔
66
        """Return true if the core is valid.
67

68
        This mostly means confirming that if any services have been started, that they are still
69
        alive.
70
        """
71
        if self._kill_switch.is_set():
×
72
            logger.error("Client failed to create a Scheduler: shutting down.")
×
73
            return False
×
74
        with self._lifecycle_lock:
×
75
            if self._services is None:
×
76
                return True
×
77
            return self._services.are_all_alive()
×
78

79
    @contextmanager
1✔
80
    def _handle_exceptions(self) -> Iterator[None]:
1✔
UNCOV
81
        try:
×
UNCOV
82
            yield
×
83
        except Exception as e:
×
84
            self._kill_switch.set()
×
85
            self._scheduler = None
×
86
            raise e
×
87

88
    def _initialize(
1✔
89
        self,
90
        bootstrap_options: OptionValueContainer,
91
        build_config: BuildConfiguration,
92
        dynamic_remote_options: DynamicRemoteOptions,
93
        scheduler_restart_explanation: str | None,
94
    ) -> None:
95
        """(Re-)Initialize the scheduler.
96

97
        Must be called under the lifecycle lock.
98
        """
UNCOV
99
        try:
×
UNCOV
100
            logger.info(
×
101
                f"{scheduler_restart_explanation}. Reinitializing scheduler..."
102
                if scheduler_restart_explanation
103
                else "Initializing scheduler..."
104
            )
UNCOV
105
            if self._services:
×
UNCOV
106
                self._services.shutdown()
×
UNCOV
107
            self._scheduler = EngineInitializer.setup_graph(
×
108
                bootstrap_options, build_config, dynamic_remote_options, self._executor
109
            )
110

UNCOV
111
            self._services = self._services_constructor(bootstrap_options, self._scheduler)
×
UNCOV
112
            logger.info("Scheduler initialized.")
×
113
        except Exception as e:
×
114
            self._kill_switch.set()
×
115
            self._scheduler = None
×
116
            raise e
×
117

118
    def prepare(
1✔
119
        self, options_bootstrapper: OptionsBootstrapper, env: CompleteEnvironmentVars
120
    ) -> tuple[GraphScheduler, OptionsInitializer]:
121
        """Get a scheduler for the given options_bootstrapper.
122

123
        Runs in a client context (generally in DaemonPantsRunner) so logging is sent to the client.
124
        """
125

UNCOV
126
        with self._handle_exceptions():
×
UNCOV
127
            build_config = self._options_initializer.build_config(options_bootstrapper, env)
×
UNCOV
128
            union_membership = UnionMembership.from_rules(build_config.union_rules)
×
UNCOV
129
            options = self._options_initializer.options(
×
130
                options_bootstrapper, env, build_config, union_membership, raise_=True
131
            )
132

UNCOV
133
        scheduler_restart_explanation: str | None = None
×
134

135
        # Because these options are computed dynamically via side effects like reading from a file,
136
        # they need to be re-evaluated every run. We only reinitialize the scheduler if changes
137
        # were made, though.
UNCOV
138
        dynamic_remote_options, auth_plugin_result = DynamicRemoteOptions.from_options(
×
139
            options,
140
            env,
141
            self._prior_auth_plugin_result,
142
            remote_auth_plugin_func=build_config.remote_auth_plugin_func,
143
        )
UNCOV
144
        remote_options_changed = dynamic_remote_options != self._prior_dynamic_remote_options
×
UNCOV
145
        if self._prior_dynamic_remote_options is not None and remote_options_changed:
×
146
            diff = summarize_dynamic_options_diff(
×
147
                self._prior_dynamic_remote_options, dynamic_remote_options
148
            )
149
            scheduler_restart_explanation = f"Remote cache/execution options updated: {diff}"
×
150

UNCOV
151
        options_map = OptionsFingerprinter.options_map_for_scope(
×
152
            GLOBAL_SCOPE,
153
            options_bootstrapper.bootstrap_options,
154
        )
UNCOV
155
        bootstrap_options_changed = options_map != self._prior_options_map
×
UNCOV
156
        if self._prior_options_map is not None and bootstrap_options_changed:
×
UNCOV
157
            diff = summarize_options_map_diff(self._prior_options_map, options_map)
×
UNCOV
158
            scheduler_restart_explanation = f"Initialization options changed: {diff}"
×
159

UNCOV
160
        with self._lifecycle_lock:
×
UNCOV
161
            if self._scheduler is None or scheduler_restart_explanation:
×
162
                # No existing options to compare (first run) or options have changed. Create a new
163
                # scheduler and services.
UNCOV
164
                bootstrap_options = options_bootstrapper.bootstrap_options.for_global_scope()
×
UNCOV
165
                assert bootstrap_options is not None
×
UNCOV
166
                with self._handle_exceptions():
×
UNCOV
167
                    self._initialize(
×
168
                        bootstrap_options,
169
                        build_config,
170
                        dynamic_remote_options,
171
                        scheduler_restart_explanation,
172
                    )
173

UNCOV
174
            self._prior_options_map = options_map
×
UNCOV
175
            self._prior_dynamic_remote_options = dynamic_remote_options
×
UNCOV
176
            self._prior_auth_plugin_result = auth_plugin_result
×
177

UNCOV
178
            assert self._scheduler is not None
×
UNCOV
179
            return self._scheduler, self._options_initializer
×
180

181
    def shutdown(self) -> None:
1✔
182
        with self._lifecycle_lock:
×
183
            if self._services is not None:
×
184
                self._services.shutdown()
×
185
                self._services = None
×
186
            if self._scheduler is not None:
×
187
                self._scheduler.scheduler.shutdown()
×
188
                self._scheduler = None
×
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

© 2025 Coveralls, Inc