|
3 | 3 | import os |
4 | 4 | import os.path |
5 | 5 | import posixpath |
6 | | -import signal |
7 | | -import subprocess |
8 | | -import sys |
9 | 6 | import time |
10 | 7 | import traceback |
11 | | -from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence |
| 8 | +from collections.abc import Callable, Iterable, Iterator, Sequence |
12 | 9 | from copy import copy |
13 | 10 | from dataclasses import dataclass |
14 | 11 | from functools import cached_property, reduce |
15 | | -from threading import Thread |
16 | | -from typing import IO, TYPE_CHECKING, Any, NoReturn |
| 12 | +from typing import TYPE_CHECKING, Any |
17 | 13 | from uuid import uuid4 |
18 | 14 |
|
19 | 15 | import sqlalchemy as sa |
20 | 16 | from sqlalchemy import Column |
21 | 17 | from tqdm.auto import tqdm |
22 | 18 |
|
23 | | -from datachain import json |
24 | 19 | from datachain.cache import Cache |
25 | 20 | from datachain.client import Client |
26 | 21 | from datachain.dataset import ( |
|
43 | 38 | DatasetVersionNotFoundError, |
44 | 39 | NamespaceNotFoundError, |
45 | 40 | ProjectNotFoundError, |
46 | | - QueryScriptCancelError, |
47 | | - QueryScriptRunError, |
48 | 41 | ) |
49 | 42 | from datachain.lib.listing import get_listing |
50 | 43 | from datachain.node import DirType, Node, NodeWithPath |
|
71 | 64 |
|
72 | 65 | INDEX_INTERNAL_ERROR_MESSAGE = "Internal error on indexing" |
73 | 66 | DATASET_INTERNAL_ERROR_MESSAGE = "Internal error on creating dataset" |
74 | | -# exit code we use if last statement in query script is not instance of DatasetQuery |
75 | | -QUERY_SCRIPT_INVALID_LAST_STATEMENT_EXIT_CODE = 10 |
76 | 67 | # exit code we use if query script was canceled |
77 | 68 | QUERY_SCRIPT_CANCELED_EXIT_CODE = 11 |
78 | 69 | QUERY_SCRIPT_SIGTERM_EXIT_CODE = -15 # if query script was terminated by SIGTERM |
|
84 | 75 | PULL_DATASET_CHECK_STATUS_INTERVAL = 20 # interval to check export status in Studio |
85 | 76 |
|
86 | 77 |
|
87 | | -def noop(_: str): |
88 | | - pass |
89 | | - |
90 | | - |
91 | | -class TerminationSignal(RuntimeError): # noqa: N818 |
92 | | - def __init__(self, signal): |
93 | | - self.signal = signal |
94 | | - super().__init__("Received termination signal", signal) |
95 | | - |
96 | | - def __repr__(self): |
97 | | - return f"{self.__class__.__name__}({self.signal})" |
98 | | - |
99 | | - |
100 | | -if sys.platform == "win32": |
101 | | - SIGINT = signal.CTRL_C_EVENT |
102 | | -else: |
103 | | - SIGINT = signal.SIGINT |
104 | | - |
105 | | - |
106 | 78 | def is_namespace_local(namespace_name) -> bool: |
107 | 79 | """Checks if namespace is from local environment, i.e. is `local`""" |
108 | 80 | return namespace_name == "local" |
109 | 81 |
|
110 | 82 |
|
111 | | -def shutdown_process( |
112 | | - proc: subprocess.Popen, |
113 | | - interrupt_timeout: int | None = None, |
114 | | - terminate_timeout: int | None = None, |
115 | | -) -> int: |
116 | | - """Shut down the process gracefully with SIGINT -> SIGTERM -> SIGKILL.""" |
117 | | - |
118 | | - logger.info("sending interrupt signal to the process %s", proc.pid) |
119 | | - proc.send_signal(SIGINT) |
120 | | - |
121 | | - logger.info("waiting for the process %s to finish", proc.pid) |
122 | | - try: |
123 | | - return proc.wait(interrupt_timeout) |
124 | | - except subprocess.TimeoutExpired: |
125 | | - logger.info( |
126 | | - "timed out waiting, sending terminate signal to the process %s", proc.pid |
127 | | - ) |
128 | | - proc.terminate() |
129 | | - try: |
130 | | - return proc.wait(terminate_timeout) |
131 | | - except subprocess.TimeoutExpired: |
132 | | - logger.info("timed out waiting, killing the process %s", proc.pid) |
133 | | - proc.kill() |
134 | | - return proc.wait() |
135 | | - |
136 | | - |
137 | | -def process_output(stream: IO[bytes], callback: Callable[[str], None]) -> None: |
138 | | - buffer = b"" |
139 | | - |
140 | | - try: |
141 | | - while byt := stream.read(1): # Read one byte at a time |
142 | | - buffer += byt |
143 | | - |
144 | | - if byt in (b"\n", b"\r"): # Check for newline or carriage return |
145 | | - line = buffer.decode("utf-8", errors="replace") |
146 | | - callback(line) |
147 | | - buffer = b"" # Clear buffer for the next line |
148 | | - |
149 | | - if buffer: # Handle any remaining data in the buffer |
150 | | - line = buffer.decode("utf-8", errors="replace") |
151 | | - callback(line) |
152 | | - finally: |
153 | | - try: |
154 | | - stream.close() # Ensure output is closed |
155 | | - except Exception: # noqa: BLE001, S110 |
156 | | - pass |
157 | | - |
158 | | - |
159 | 83 | class DatasetRowsFetcher(NodesThreadPool): |
160 | 84 | def __init__( |
161 | 85 | self, |
@@ -1781,120 +1705,6 @@ def clone( |
1781 | 1705 | recursive=recursive, |
1782 | 1706 | ) |
1783 | 1707 |
|
1784 | | - @staticmethod |
1785 | | - def query( |
1786 | | - query_script: str, |
1787 | | - env: Mapping[str, str] | None = None, |
1788 | | - python_executable: str = sys.executable, |
1789 | | - stdout_callback: Callable[[str], None] | None = None, |
1790 | | - stderr_callback: Callable[[str], None] | None = None, |
1791 | | - params: dict[str, str] | None = None, |
1792 | | - job_id: str | None = None, |
1793 | | - reset: bool = False, |
1794 | | - interrupt_timeout: int | None = None, |
1795 | | - terminate_timeout: int | None = None, |
1796 | | - ) -> None: |
1797 | | - if not isinstance(reset, bool): |
1798 | | - raise TypeError(f"reset must be a bool, got {type(reset).__name__}") |
1799 | | - |
1800 | | - cmd = [python_executable, "-c", query_script] |
1801 | | - env = dict(env or os.environ) |
1802 | | - env.update( |
1803 | | - { |
1804 | | - "DATACHAIN_QUERY_PARAMS": json.dumps(params or {}), |
1805 | | - "DATACHAIN_JOB_ID": job_id or "", |
1806 | | - "DATACHAIN_CHECKPOINTS_RESET": str(reset), |
1807 | | - }, |
1808 | | - ) |
1809 | | - popen_kwargs: dict[str, Any] = {} |
1810 | | - |
1811 | | - if stdout_callback is not None: |
1812 | | - popen_kwargs = {"stdout": subprocess.PIPE} |
1813 | | - if stderr_callback is not None: |
1814 | | - popen_kwargs["stderr"] = subprocess.PIPE |
1815 | | - |
1816 | | - def raise_termination_signal(sig: int, _: Any) -> NoReturn: |
1817 | | - raise TerminationSignal(sig) |
1818 | | - |
1819 | | - stdout_thread: Thread | None = None |
1820 | | - stderr_thread: Thread | None = None |
1821 | | - |
1822 | | - with subprocess.Popen(cmd, env=env, **popen_kwargs) as proc: # noqa: S603 |
1823 | | - logger.info("Starting process %s", proc.pid) |
1824 | | - |
1825 | | - orig_sigint_handler = signal.getsignal(signal.SIGINT) |
1826 | | - # ignore SIGINT in the main process. |
1827 | | - # In the terminal, SIGINTs are received by all the processes in |
1828 | | - # the foreground process group, so the script will receive the signal too. |
1829 | | - # (If we forward the signal to the child, it will receive it twice.) |
1830 | | - signal.signal(signal.SIGINT, signal.SIG_IGN) |
1831 | | - |
1832 | | - orig_sigterm_handler = signal.getsignal(signal.SIGTERM) |
1833 | | - signal.signal(signal.SIGTERM, raise_termination_signal) |
1834 | | - try: |
1835 | | - if stdout_callback is not None: |
1836 | | - stdout_thread = Thread( |
1837 | | - target=process_output, |
1838 | | - args=(proc.stdout, stdout_callback), |
1839 | | - daemon=True, |
1840 | | - ) |
1841 | | - stdout_thread.start() |
1842 | | - if stderr_callback is not None: |
1843 | | - stderr_thread = Thread( |
1844 | | - target=process_output, |
1845 | | - args=(proc.stderr, stderr_callback), |
1846 | | - daemon=True, |
1847 | | - ) |
1848 | | - stderr_thread.start() |
1849 | | - |
1850 | | - proc.wait() |
1851 | | - except TerminationSignal as exc: |
1852 | | - signal.signal(signal.SIGTERM, orig_sigterm_handler) |
1853 | | - signal.signal(signal.SIGINT, orig_sigint_handler) |
1854 | | - logger.info("Shutting down process %s, received %r", proc.pid, exc) |
1855 | | - # Rather than forwarding the signal to the child, we try to shut it down |
1856 | | - # gracefully. This is because we consider the script to be interactive |
1857 | | - # and special, so we give it time to cleanup before exiting. |
1858 | | - shutdown_process(proc, interrupt_timeout, terminate_timeout) |
1859 | | - if proc.returncode: |
1860 | | - raise QueryScriptCancelError( |
1861 | | - "Query script was canceled by user", return_code=proc.returncode |
1862 | | - ) from exc |
1863 | | - finally: |
1864 | | - signal.signal(signal.SIGTERM, orig_sigterm_handler) |
1865 | | - signal.signal(signal.SIGINT, orig_sigint_handler) |
1866 | | - # wait for the reader thread |
1867 | | - thread_join_timeout_seconds = 30 |
1868 | | - if stdout_thread is not None: |
1869 | | - stdout_thread.join(timeout=thread_join_timeout_seconds) |
1870 | | - if stdout_thread.is_alive(): |
1871 | | - logger.warning( |
1872 | | - "stdout thread is still alive after %s seconds", |
1873 | | - thread_join_timeout_seconds, |
1874 | | - ) |
1875 | | - if stderr_thread is not None: |
1876 | | - stderr_thread.join(timeout=thread_join_timeout_seconds) |
1877 | | - if stderr_thread.is_alive(): |
1878 | | - logger.warning( |
1879 | | - "stderr thread is still alive after %s seconds", |
1880 | | - thread_join_timeout_seconds, |
1881 | | - ) |
1882 | | - |
1883 | | - logger.info("Process %s exited with return code %s", proc.pid, proc.returncode) |
1884 | | - if proc.returncode in ( |
1885 | | - QUERY_SCRIPT_CANCELED_EXIT_CODE, |
1886 | | - QUERY_SCRIPT_SIGTERM_EXIT_CODE, |
1887 | | - ): |
1888 | | - raise QueryScriptCancelError( |
1889 | | - "Query script was canceled by user", |
1890 | | - return_code=proc.returncode, |
1891 | | - ) |
1892 | | - if proc.returncode: |
1893 | | - raise QueryScriptRunError( |
1894 | | - f"Query script exited with error code {proc.returncode}", |
1895 | | - return_code=proc.returncode, |
1896 | | - ) |
1897 | | - |
1898 | 1708 | def cp( |
1899 | 1709 | self, |
1900 | 1710 | sources: list[str], |
|
0 commit comments