@@ -494,7 +494,6 @@ def on_failure(self, exc, task_id, args, kwargs, einfo):
494
494
if self .data .version :
495
495
version_type = self .data .version .type
496
496
497
- # NOTE: autoflake gets confused here. We need the NOQA for now.
498
497
status = BUILD_STATUS_FAILURE
499
498
if isinstance (exc , BuildUserSkip ):
500
499
# The build was skipped by returning the magic exit code,
@@ -537,56 +536,41 @@ def get_valid_artifact_types(self):
537
536
continue
538
537
539
538
if not os .path .isdir (artifact_directory ):
540
- log .exception (
539
+ log .error (
541
540
"The output path is not a directory." ,
542
541
output_format = artifact_type ,
543
542
)
544
- # TODO: we should raise an exception here, fail the build,
545
- # and communicate the error to the user.
546
- # We are just skipping this output format for now.
547
- #
548
- # raise BuildUserError(BuildUserError.BUILD_OUTPUT_IS_NOT_A_DIRECTORY.format(artifact_type))
549
- continue
543
+ raise BuildUserError (
544
+ BuildUserError .BUILD_OUTPUT_IS_NOT_A_DIRECTORY .format (
545
+ artifact_type = artifact_type
546
+ )
547
+ )
550
548
551
549
# Check if there are multiple files on artifact directories.
552
550
# These output format does not support multiple files yet.
553
551
# In case multiple files are found, the upload for this format is not performed.
554
- #
555
- # TODO: we should fail the build for these cases and clearly communicate this.
556
- # to the user. To do this, we need to call this method (``store_build_artifacts``)
557
- # since the ``execute`` method.
558
- # It will allow us to just `raise BuildUserError` and handle it at
559
- # Celery `on_failure` handler.
560
552
if artifact_type in ("htmlzip" , "epub" , "pdf" ):
561
553
if len (os .listdir (artifact_directory )) > 1 :
562
- log .exception (
554
+ log .error (
563
555
"Multiple files are not supported for this format. "
564
556
"Skipping this output format." ,
565
557
output_format = artifact_type ,
566
558
)
567
- continue
559
+ raise BuildUserError (
560
+ BuildUserError .BUILD_OUTPUT_HAS_MULTIPLE_FILES .format (
561
+ artifact_type = artifact_type
562
+ )
563
+ )
568
564
569
565
# If all the conditions were met, the artifact is valid
570
566
valid_artifacts .append (artifact_type )
571
567
572
568
return valid_artifacts
573
569
574
570
def on_success (self , retval , task_id , args , kwargs ):
575
-
576
- valid_artifacts = self .get_valid_artifact_types ()
577
- time_before_store_build_artifacts = timezone .now ()
578
- # Store build artifacts to storage (local or cloud storage)
579
- #
580
- # TODO: move ``store_build_artifacts`` inside ``execute`` so we can
581
- # handle exceptions properly on ``on_failure``
582
- self .store_build_artifacts (valid_artifacts )
583
- log .info (
584
- "Store build artifacts finished." ,
585
- time = (timezone .now () - time_before_store_build_artifacts ).seconds ,
586
- )
587
-
588
571
# NOTE: we are updating the db version instance *only* when
589
- if "html" in valid_artifacts :
572
+ # TODO: remove this condition and *always* update the DB Version instance
573
+ if "html" in self .get_valid_artifact_types ():
590
574
try :
591
575
api_v2 .version (self .data .version .pk ).patch (
592
576
{
@@ -601,7 +585,8 @@ def on_success(self, retval, task_id, args, kwargs):
601
585
# NOTE: I think we should fail the build if we cannot update
602
586
# the version at this point. Otherwise, we will have inconsistent data
603
587
log .exception (
604
- 'Updating version failed, skipping file sync.' ,
588
+ "Updating version db object failed. "
589
+ 'Files are synced in the storage, but "Version" object is not updated' ,
605
590
)
606
591
607
592
# Index search data
@@ -698,10 +683,13 @@ def update_build(self, state=None):
698
683
try :
699
684
api_v2 .build (self .data .build ['id' ]).patch (self .data .build )
700
685
except Exception :
701
- # NOTE: I think we should fail the build if we cannot update it
702
- # at this point otherwise, the data will be inconsistent and we
703
- # may be serving "new docs" but saying the "build have failed"
704
- log .exception ('Unable to update build' )
686
+ # NOTE: we are updating the "Build" object on each `state`.
687
+ # Only if the last update fails, there may be some inconsistency
688
+ # between the "Build" object in our db and the reality.
689
+ #
690
+ # The `state` argument will help us to track this more and understand
691
+ # at what state our updates are failing and decide what to do.
692
+ log .exception ("Error while updating the build object." , state = state )
705
693
706
694
def execute (self ):
707
695
self .data .build_director = BuildDirector (
@@ -749,6 +737,12 @@ def execute(self):
749
737
finally :
750
738
self .data .build_data = self .collect_build_data ()
751
739
740
+ # At this point, the user's build already succeeded.
741
+ # However, we cannot use `.on_success()` because we still have to upload the artifacts;
742
+ # which could fail, and we want to detect that and handle it properly at `.on_failure()`
743
+ # Store build artifacts to storage (local or cloud storage)
744
+ self .store_build_artifacts ()
745
+
752
746
def collect_build_data (self ):
753
747
"""
754
748
Collect data from the current build.
@@ -817,7 +811,7 @@ def set_valid_clone(self):
817
811
self .data .project .has_valid_clone = True
818
812
self .data .version .project .has_valid_clone = True
819
813
820
- def store_build_artifacts (self , artifacts ):
814
+ def store_build_artifacts (self ):
821
815
"""
822
816
Save build artifacts to "storage" using Django's storage API.
823
817
@@ -826,17 +820,16 @@ def store_build_artifacts(self, artifacts):
826
820
827
821
Remove build artifacts of types not included in this build (PDF, ePub, zip only).
828
822
"""
823
+ time_before_store_build_artifacts = timezone .now ()
829
824
log .info ('Writing build artifacts to media storage' )
830
- # NOTE: I don't remember why we removed this state from the Build
831
- # object. I'm re-adding it because I think it's useful, but we can
832
- # remove it if we want
833
825
self .update_build (state = BUILD_STATE_UPLOADING )
834
826
827
+ valid_artifacts = self .get_valid_artifact_types ()
835
828
types_to_copy = []
836
829
types_to_delete = []
837
830
838
831
for artifact_type in ARTIFACT_TYPES :
839
- if artifact_type in artifacts :
832
+ if artifact_type in valid_artifacts :
840
833
types_to_copy .append (artifact_type )
841
834
# Never delete HTML nor JSON (search index)
842
835
elif artifact_type not in UNDELETABLE_ARTIFACT_TYPES :
@@ -857,14 +850,20 @@ def store_build_artifacts(self, artifacts):
857
850
try :
858
851
build_media_storage .sync_directory (from_path , to_path )
859
852
except Exception :
860
- # Ideally this should just be an IOError
861
- # but some storage backends unfortunately throw other errors
853
+ # NOTE: the exceptions reported so far are:
854
+ # - botocore.exceptions:HTTPClientError
855
+ # - botocore.exceptions:ClientError
856
+ # - readthedocs.doc_builder.exceptions:BuildCancelled
862
857
log .exception (
863
- ' Error copying to storage (not failing build)' ,
858
+ " Error copying to storage" ,
864
859
media_type = media_type ,
865
860
from_path = from_path ,
866
861
to_path = to_path ,
867
862
)
863
+ # Re-raise the exception to fail the build and handle it
864
+ # automatically at `on_failure`.
865
+ # It will clearly communicate the error to the user.
866
+ raise BuildAppError ("Error uploading files to the storage." )
868
867
869
868
# Delete formats
870
869
for media_type in types_to_delete :
@@ -877,13 +876,21 @@ def store_build_artifacts(self, artifacts):
877
876
try :
878
877
build_media_storage .delete_directory (media_path )
879
878
except Exception :
880
- # Ideally this should just be an IOError
881
- # but some storage backends unfortunately throw other errors
879
+ # NOTE: I didn't find any log line for this case yet
882
880
log .exception (
883
- ' Error deleting from storage (not failing build)' ,
881
+ " Error deleting files from storage" ,
884
882
media_type = media_type ,
885
883
media_path = media_path ,
886
884
)
885
+ # Re-raise the exception to fail the build and handle it
886
+ # automatically at `on_failure`.
887
+ # It will clearly communicate the error to the user.
888
+ raise BuildAppError ("Error deleting files from storage." )
889
+
890
+ log .info (
891
+ "Store build artifacts finished." ,
892
+ time = (timezone .now () - time_before_store_build_artifacts ).seconds ,
893
+ )
887
894
888
895
def send_notifications (self , version_pk , build_pk , event ):
889
896
"""Send notifications to all subscribers of `event`."""
0 commit comments