Handle errors in capture thread
This commit is contained in:
parent
5dd23f6ef4
commit
ff1184d453
2 changed files with 113 additions and 14 deletions
|
|
@ -13,7 +13,7 @@ from PIL import Image
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from subprocess import CompletedProcess, Popen, PIPE, SubprocessError, STDOUT
|
from subprocess import CompletedProcess, Popen, PIPE, SubprocessError, STDOUT
|
||||||
from threading import Event, Thread
|
from threading import Event
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
import piexif
|
import piexif
|
||||||
|
|
@ -30,6 +30,8 @@ from labthings_fastapi.decorators import thing_action, thing_property, fastapi_e
|
||||||
from labthings_fastapi.outputs.blob import blob_type
|
from labthings_fastapi.outputs.blob import blob_type
|
||||||
from .camera import CameraDependency as CamDep
|
from .camera import CameraDependency as CamDep
|
||||||
from .stage import StageDependency as StageDep
|
from .stage import StageDependency as StageDep
|
||||||
|
|
||||||
|
from openflexure_microscope_server.utilities import ErrorCapturingThread
|
||||||
from openflexure_microscope_server.things.autofocus import AutofocusThing
|
from openflexure_microscope_server.things.autofocus import AutofocusThing
|
||||||
from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper
|
from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper
|
||||||
from openflexure_microscope_server.things.background_detect import BackgroundDetectThing
|
from openflexure_microscope_server.things.background_detect import BackgroundDetectThing
|
||||||
|
|
@ -290,6 +292,7 @@ class SmartScanThing(Thing):
|
||||||
"""
|
"""
|
||||||
# Define these variables so we can use them in the finally: block
|
# Define these variables so we can use them in the finally: block
|
||||||
# (after testing they are not None)
|
# (after testing they are not None)
|
||||||
|
scan_sucessful = True
|
||||||
scan_folder = None
|
scan_folder = None
|
||||||
images_folder = None
|
images_folder = None
|
||||||
starting_position = None
|
starting_position = None
|
||||||
|
|
@ -483,9 +486,15 @@ class SmartScanThing(Thing):
|
||||||
|
|
||||||
# Acquire the image in a thread, and continue once it's acquired (i.e. leave saving in the background)
|
# Acquire the image in a thread, and continue once it's acquired (i.e. leave saving in the background)
|
||||||
if capture_thread: # wait for the previous capture to be saved, i.e. don't leave more than one image saving in the background
|
if capture_thread: # wait for the previous capture to be saved, i.e. don't leave more than one image saving in the background
|
||||||
if capture_thread.is_alive():
|
wait_start = time.time()
|
||||||
wait_start = time.time()
|
thread_was_alive = capture_thread.is_alive()
|
||||||
capture_thread.join()
|
|
||||||
|
# If the capture thread has thrown an exception it will be raised when join is called,
|
||||||
|
# this will cause the scan to end. If we want to retry captures at a later date
|
||||||
|
# this is where we will need to catch the IOError or CaptureError from the thread.
|
||||||
|
capture_thread.join()
|
||||||
|
time.sleep(0.2)
|
||||||
|
if thread_was_alive:
|
||||||
wait_time = time.time() - wait_start
|
wait_time = time.time() - wait_start
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Waited {wait_time:.1f}s for the previous capture to finish saving."
|
f"Waited {wait_time:.1f}s for the previous capture to finish saving."
|
||||||
|
|
@ -494,7 +503,10 @@ class SmartScanThing(Thing):
|
||||||
name = f"image_{loc[0]}_{loc[1]}.jpg"
|
name = f"image_{loc[0]}_{loc[1]}.jpg"
|
||||||
jpeg_path = os.path.join(images_folder, name)
|
jpeg_path = os.path.join(images_folder, name)
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
capture_thread = Thread(
|
|
||||||
|
# Use ErrorCapturingThread intead of Thread. This will raise errors in the calling
|
||||||
|
# thread only when join() is called, allowing us to handle this appropriately.
|
||||||
|
capture_thread = ErrorCapturingThread(
|
||||||
target=self.capture_and_save,
|
target=self.capture_and_save,
|
||||||
kwargs={
|
kwargs={
|
||||||
"acquired": acquired,
|
"acquired": acquired,
|
||||||
|
|
@ -534,23 +546,39 @@ class SmartScanThing(Thing):
|
||||||
)
|
)
|
||||||
|
|
||||||
except InvocationCancelledError:
|
except InvocationCancelledError:
|
||||||
|
scan_sucessful = False
|
||||||
logger.error("Stopping scan because it was cancelled.")
|
logger.error("Stopping scan because it was cancelled.")
|
||||||
except NotEnoughFreeSpaceError as e:
|
except NotEnoughFreeSpaceError as e:
|
||||||
|
scan_sucessful = False
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Stopping scan to avoid filling up the disk: {e}",
|
f"Stopping scan to avoid filling up the disk: {e}",
|
||||||
exc_info=e,
|
exc_info=e,
|
||||||
)
|
)
|
||||||
raise e
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
scan_sucessful = False
|
||||||
logger.error(
|
logger.error(
|
||||||
f"The scan stopped because of an error: {e}",
|
f"The scan stopped because of an error: {e} "
|
||||||
"We will attempt to stitch and archive the images acquired so far.",
|
"Attempting to stitch and archive the images acquired so far.",
|
||||||
exc_info=e,
|
exc_info=e,
|
||||||
)
|
)
|
||||||
raise e
|
raise e
|
||||||
finally:
|
finally:
|
||||||
if capture_thread:
|
if capture_thread:
|
||||||
capture_thread.join()
|
# If the capture thread had an error we capture it here
|
||||||
|
try:
|
||||||
|
capture_thread.join()
|
||||||
|
except Exception as e:
|
||||||
|
# If the thread has already ended due to an exception we will
|
||||||
|
# ignore any excppetions, but if it appeared to be succesfull
|
||||||
|
# we will log an error.
|
||||||
|
if scan_sucessful:
|
||||||
|
logger.error(
|
||||||
|
"The appears to have been successful however the final capture raised"
|
||||||
|
f"the following error: {e}."
|
||||||
|
"Attempting to stitch and archive images.",
|
||||||
|
exc_info=e,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
logger.info("Returning to starting position.")
|
logger.info("Returning to starting position.")
|
||||||
if starting_position is not None:
|
if starting_position is not None:
|
||||||
|
|
@ -606,12 +634,9 @@ class SmartScanThing(Thing):
|
||||||
logger: InvocationLogger,
|
logger: InvocationLogger,
|
||||||
) -> tuple[np.ndarray, dict]:
|
) -> tuple[np.ndarray, dict]:
|
||||||
"""Capture an image in memory and return it with metadata
|
"""Capture an image in memory and return it with metadata
|
||||||
|
|
||||||
This will set the event `acquired` once the image has been acquired, so
|
This will set the event `acquired` once the image has been acquired, so
|
||||||
that the stage may be moved while it's saved.
|
that the stage may be moved while it's saved.
|
||||||
|
|
||||||
CaptureError raised if the capture fails for any reason
|
CaptureError raised if the capture fails for any reason
|
||||||
|
|
||||||
returns tuple with numpy array of image data, and dict of metadata
|
returns tuple with numpy array of image data, and dict of metadata
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
|
@ -631,11 +656,8 @@ class SmartScanThing(Thing):
|
||||||
logger: InvocationLogger,
|
logger: InvocationLogger,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Saving the captured image and metadata to disk
|
"""Saving the captured image and metadata to disk
|
||||||
|
|
||||||
logger warning (via InvocationLogger) is raised if metadata is failed to be added
|
logger warning (via InvocationLogger) is raised if metadata is failed to be added
|
||||||
|
|
||||||
IOError is raised if the file cannot be saved
|
IOError is raised if the file cannot be saved
|
||||||
|
|
||||||
nothing is returned on success"""
|
nothing is returned on success"""
|
||||||
try:
|
try:
|
||||||
Image.fromarray(image.astype("uint8"), "RGB").save(
|
Image.fromarray(image.astype("uint8"), "RGB").save(
|
||||||
|
|
|
||||||
77
src/openflexure_microscope_server/utilities.py
Normal file
77
src/openflexure_microscope_server/utilities.py
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
"""
|
||||||
|
This module contains some utility functions and classes
|
||||||
|
"""
|
||||||
|
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorCapturingThread(Thread):
|
||||||
|
"""
|
||||||
|
This is a a subclass or Thread. It wraps the target function in a
|
||||||
|
try-except block.
|
||||||
|
|
||||||
|
Execution will stop with an unhandled exception, but the exception will not be raised
|
||||||
|
until the join method is called. When the join method is called, the exception is
|
||||||
|
called in the calling thread.
|
||||||
|
|
||||||
|
This allows for error handling to be performed by the calling thread at the time that
|
||||||
|
join is run.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, group=None, target=None, args=None, kwargs=None, daemon=None):
|
||||||
|
"""
|
||||||
|
Initialise with the same arguments as Thread
|
||||||
|
"""
|
||||||
|
# As all inputs are keywords we need to set the default values for args and kwargs:
|
||||||
|
if args is None:
|
||||||
|
args = ()
|
||||||
|
if kwargs is None:
|
||||||
|
kwargs = {}
|
||||||
|
# Make and empy list for the error.
|
||||||
|
self._error_buffer = []
|
||||||
|
|
||||||
|
# Add the target function end error buffer to the thread to the start of
|
||||||
|
# the argument list so they are passed to _wrap_and_catch_errors()
|
||||||
|
args = (target, self._error_buffer, *args)
|
||||||
|
# Start the thread with _wrap_and_catch_errors() as the target
|
||||||
|
super().__init__(
|
||||||
|
group=group,
|
||||||
|
target=_wrap_and_catch_errors,
|
||||||
|
args=args,
|
||||||
|
kwargs=kwargs,
|
||||||
|
daemon=daemon,
|
||||||
|
)
|
||||||
|
|
||||||
|
def join(self, timeout=None):
|
||||||
|
"""
|
||||||
|
Join when the thread is complete. If the thread ended due to an unhandled exception,
|
||||||
|
the exception will be raised when this method is called.
|
||||||
|
"""
|
||||||
|
super().join(timeout)
|
||||||
|
# If there is an error in the error buffer clear the buffer and raise it
|
||||||
|
if self._error_buffer:
|
||||||
|
err = self._error_buffer[0]
|
||||||
|
self._error_buffer = []
|
||||||
|
raise err
|
||||||
|
|
||||||
|
|
||||||
|
def _wrap_and_catch_errors(target, error_buffer, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
This function is designed only to be used by
|
||||||
|
ErrorCapturingThread
|
||||||
|
|
||||||
|
It will run a target function in a try-except block
|
||||||
|
any errors caught are added to an empty list that
|
||||||
|
should be supplied as a keyword argument
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
* target - The target function to call
|
||||||
|
* error_buffer - An empty list that is used for returning
|
||||||
|
the exception from the thread
|
||||||
|
*args: The arguments for the target function
|
||||||
|
**keargs: The Keyword arguments for the target function
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
target(*args, **kwargs)
|
||||||
|
except BaseException as e:
|
||||||
|
error_buffer.append(e)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue