add ewoksorange.gui.concurrency.executor module#405
Conversation
…ecutor adapted to ewoks tasks
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
| (ThreadPoolExecutor, 4, SubmitPolicy.ALWAYS), id="thread-4-parallel" | ||
| ), | ||
| pytest.param( | ||
| (ProcessPoolExecutor, 2, SubmitPolicy.ALWAYS), id="process-2-parallel" |
There was a problem hiding this comment.
I only added one test with the ProcessPoolExecutor as mentioned in the code this is more some code to show the way to go but most likely some time will be needed to check possible bottlenecks.
| started_queue = manager.Queue() | ||
| abort_event = manager.Event() |
There was a problem hiding this comment.
At the time each time we submit a task we will need the creation of those two objects (manager.Queue and manager.Event) that from what I have on mind can take a bit of time.
I think in general as written on the doc the ProcessPool is more a prototype in order to 'show the way'.
| task_class, task_kwargs, started_queue, abort_event | ||
| ) | ||
| worker = _EwoksProcessWorker(abort_event) | ||
| self_ref = weakref.ref(self) |
There was a problem hiding this comment.
used to emit the requested signal downstream
| ) | ||
| worker = _EwoksProcessWorker(abort_event) | ||
| self_ref = weakref.ref(self) | ||
| _holder: list = [None] |
There was a problem hiding this comment.
small hack to get the task future defined downstream and needed at execution time.
* split src.ewoksorange.gui.concurrency.EwoksExecutor to a 'executor' module * add: * ProcessCallable: Top-level picklable callable submitted to ProcessPoolExecutor. * EwoksProcessWorker: worker for the process
9b20747 to
8c1c146
Compare
ewoksorange.gui.concurrency.executor module
| self._task_class = task_class | ||
| self._task_kwargs = task_kwargs | ||
| self._task: Optional[Task] = None | ||
| self._lock = Lock() |
There was a problem hiding this comment.
It makes sure there is no conflict when abort is called (if _task is not created yet in call).
| _Var = namedtuple("_Var", ["value"]) | ||
|
|
||
|
|
||
| class ProcessCallable: |
There was a problem hiding this comment.
This part is needed for submitting process only.
| self._started_queue = started_queue | ||
| self._abort_event = abort_event | ||
|
|
||
| def __call__(self): |
There was a problem hiding this comment.
This is the implementation proposed by Claude. The combination of thread / process seems a bit heavy.
An alternative could be to say that processes are not cancelled to not use the task.cancel and fill directly the process (using process.kill).
But again I think this submission to process is more a demo at the moment and to be modified by people concerned about this use case as they will probably have specific use cases.
| succeeded = Signal(object, object) | ||
| failed = Signal(object, object) | ||
| ignored = Signal() | ||
| finished = Signal(object) |
There was a problem hiding this comment.
An alternative could be to return the finished with a state (succeeded or failed) but this won't make sense to return the results or exception.
| try: | ||
| result = raw_future.result() | ||
| except Exception as exc: | ||
| self.failed.emit(task_future, exc) | ||
| else: | ||
| self.succeeded.emit(task_future, result) | ||
| finally: | ||
| self.finished.emit(task_future) |
There was a problem hiding this comment.
Maybe this part could be simplified by only emitting finished and the future. From this future we can retrieve the exception or result.
ewoksorange.gui.concurrency.executor moduleewoksorange.gui.concurrency.executor module
|
Oef, much more complex than my proposal #403 (comment) In the spirit of "separation of concerns" could be make the executor Ewoks agnostic? Can you list the pro's of this approach with respect to #403 (comment) ? If we can't use the existing from concurrent.futures import ProcessPoolExecutor
from concurrent.futures import ThreadPoolExecutorcan we at least derive executors from from concurrent.futures import Executorso we don't have to invent things like Could you also add a standalone example similar to this one in the scripts folder? |
|
Well I think the key point is that we want to be able to call the ewoks task And this is why we have this Behind it we use it as: EwoksExecutor(
ThreadPoolExecutor(max_workers=self._MAX_WORKERS), # or ProcessPoolExecutor
self._SUBMIT_POLICY,
)Then the Process iteration also add some complexity because it requires the Here is the 'gui' updated with the code you had on your PR. import logging
import sys
from concurrent.futures import ProcessPoolExecutor
from concurrent.futures import ThreadPoolExecutor
from AnyQt.QtWidgets import QApplication
from AnyQt.QtWidgets import QGridLayout
from AnyQt.QtWidgets import QPushButton
from AnyQt.QtWidgets import QTextEdit
from AnyQt.QtWidgets import QVBoxLayout
from AnyQt.QtWidgets import QWidget
from ewokscore.tests.examples.tasks.sumtask import SumTask
from ewoksorange.gui.concurrency.executor import EwoksExecutor, SubmitPolicy
class Window(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("EwoksExecutor Demo")
self.log = QTextEdit(readOnly=True)
self.executors = {
"T DROP": EwoksExecutor(
ThreadPoolExecutor(max_workers=1),
SubmitPolicy.DROP_IF_BUSY,
),
"T QUEUE": EwoksExecutor(
ThreadPoolExecutor(max_workers=1),
SubmitPolicy.ALWAYS,
),
"T PARALLEL": EwoksExecutor(
ThreadPoolExecutor(max_workers=4),
SubmitPolicy.ALWAYS,
),
"P DROP": EwoksExecutor(
ProcessPoolExecutor(max_workers=1),
SubmitPolicy.DROP_IF_BUSY,
),
"P QUEUE": EwoksExecutor(
ProcessPoolExecutor(max_workers=1),
SubmitPolicy.ALWAYS,
),
"P PARALLEL": EwoksExecutor(
ProcessPoolExecutor(max_workers=4),
SubmitPolicy.ALWAYS,
),
}
for name, executor in self.executors.items():
executor.submitted.connect(
lambda future, n=name: self.on_submitted(n, future)
)
executor.ignored.connect(lambda n=name: self.log.append(f"[{n}] ⚠ Ignored"))
executor.succeeded.connect(
lambda future, result, n=name: self.log.append(
f"[{n}] ✅ {id(future)} -> { {k: v.value for k, v in result.items()} }"
)
)
executor.failed.connect(
lambda future, exc, n=name: self.log.append(
f"[{n}] ❌ {id(future)} -> {exc}"
)
)
layout = QVBoxLayout(self)
grid = QGridLayout()
buttons = [
("Submit x5 (Thread Drop)", "T DROP"),
("Submit x5 (Thread Queue)", "T QUEUE"),
("Submit x5 (Thread Parallel)", "T PARALLEL"),
("Submit x5 (Process Drop)", "P DROP"),
("Submit x5 (Process Queue)", "P QUEUE"),
("Submit x5 (Process Parallel)", "P PARALLEL"),
]
for i, (text, key) in enumerate(buttons):
button = QPushButton(text)
button.clicked.connect(
lambda checked=False, k=key: self.submit_many(self.executors[k])
)
grid.addWidget(button, i // 2, i % 2)
layout.addLayout(grid)
layout.addWidget(self.log)
self.counter = 1
def on_submitted(self, name, future):
if future is None:
self.log.append(f"[{name}] Submitted -> None")
else:
self.log.append(f"[{name}] Submitted -> {id(future)}")
def submit_many(self, executor):
for _ in range(5):
executor.submit_task(
SumTask, inputs={"a": self.counter, "b": self.counter, "delay": 2}
)
self.counter += 1
def closeEvent(self, event):
for executor in self.executors.values():
executor.shutdown()
super().closeEvent(event)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
app = QApplication(sys.argv)
window = Window()
window.resize(900, 500)
window.show()
sys.exit(app.exec()) |
|
We can remove the execution on Process. This is what add the most complexity (see commit e3555ab) |
|
Ok my bad, I saw a raw threading. Thread and queue and thought we were re-implementing the executor logic from python. |
| except Exception as exc: | ||
| # ewokscore wraps run() exceptions in RuntimeError; re-raise the | ||
| # original so callers see the same type as the nothread path. | ||
| original = task.exception | ||
| raise original if original is not None else exc |
There was a problem hiding this comment.
Remove try-except. The raises exception is task.exception. We should fix TaskExecutor accordingly. I can do that in a follow-up.
There was a problem hiding this comment.
Thanks for the hard work. Perhaps best to discuss in person because there are some tricky things in the current implementation.
Script
Could you make a scripts folder and add the example?
Task execution logic
When executing a task without cancel/abort, this happens:
- submit task to executor
- emit "submitted" Qt signal with future are argument
- emit "started" Qt signal with future are argument
- emit either
- "failed" Qt signal with future and exception as argument
- "succeeded" Qt signal with future and
task.output_variablesas arguments
- emit "finished" with future as argument
The execution state is wrapped with a EwoksWorker. It is intended to be executed once.
The TaskFuture has a reference to the native future and the EwoksWorker.
The EwoksWorker exposes abort() which is exposed in turn by TaskFuture.
When calling TaskFuture.cancel() the task gets dropped from the native executor but we don't has a Qt signal for that. Also the implementation of the submision blocks until the task is actually running. That should be fixed.
Also TaskFuture.abort() returns True when the task is reached and aborted. This is not the case for threads.
Also we need to handle the case where the task gets aborted before it even gets scheduled but the native executor.
Is TaskFuture.abort() supposed to be a blocking call? If not how can it return True or False, unless there is an "_aborted" state?
Harmonization
Both EwoksExecutor._submit_thread and EwoksExecutor._submit_process achieve
the same thing but it is very hard to know from reading the code.
EwoksThreadWorker->ProcessCallable+EwoksProcessWorkerrun->relay_started+ProcessCallable
The logic is valid (except that it blocks until the task is running which needs fixing) but needs refactoring for readability and harmony.
That sounds like an ideal AI job.
If I would do it manually I would start by
- making an abstract
EwoksWorker - rename "worker" to something that reflects what it actually does (this will illustrate what needs to be refactored)
- use that abstract type as a type annotation in
TaskFuture.__init__ - then the refactoring
…s and remove the 'has_task' function.
57e77b8 to
e9d5528
Compare
… signal when the future is aborted
…ring task execution
2288dc5 to
b6b8870
Compare
4fd5344 to
ca92156
Compare
There was a problem hiding this comment.
I made a PR to this PR to add a thread cleanup fix script and an example: #411.
I'm still wondering whether the split of _EwoksThreadHandle into _EwoksProcessHandle and _ProcessCallable cannot be done in a cleaner way. Perhaps split _EwoksThreadHandle in the same way for consistency reasons. But this is internal API and can be changed at any time. Functionally everything is sound now. LGTM after #411.
Thanks for the huge effort!
…-provide-api-to-cancel-a-task-3-add_example 387 owewokswidgetwithtaskstack provide api to cancel a task 3 add example
This PR; done with Claude support add a
gui.concurrency.executormodule.This module include mechanism for execution in the context of ewoks tasks.
The goal is to replace the existing widget executors by this module and add a "demo" for the multi-process. Update of existing will be done in another dedicated PR.
Preparatory PR for #406
AI disclosure: Done using Claude