@@ -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,
@@ -526,6 +525,7 @@ def get_valid_artifact_types(self):
526
525
Add support for multiple PDF files in the output directory and
527
526
grab them by using glob syntaxt between other files that could be garbage.
528
527
"""
528
+ valid_artifacts = []
529
529
for artifact_type in ARTIFACT_TYPES :
530
530
artifact_directory = self .data .project .artifact_path (
531
531
version = self .data .version .slug ,
@@ -537,55 +537,42 @@ def get_valid_artifact_types(self):
537
537
continue
538
538
539
539
if not os .path .isdir (artifact_directory ):
540
- log .exception (
540
+ log .error (
541
541
"The output path is not a directory." ,
542
542
output_format = artifact_type ,
543
543
)
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
544
+ raise BuildUserError (
545
+ BuildUserError .BUILD_OUTPUT_IS_NOT_A_DIRECTORY .format (
546
+ artifact_type = artifact_type
547
+ )
548
+ )
550
549
551
550
# Check if there are multiple files on artifact directories.
552
551
# These output format does not support multiple files yet.
553
552
# 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
553
if artifact_type in ("htmlzip" , "epub" , "pdf" ):
561
554
if len (os .listdir (artifact_directory )) > 1 :
562
- log .exception (
555
+ log .error (
563
556
"Multiple files are not supported for this format. "
564
557
"Skipping this output format." ,
565
558
output_format = artifact_type ,
566
559
)
567
- continue
560
+ raise BuildUserError (
561
+ BuildUserError .BUILD_OUTPUT_HAS_MULTIPLE_FILES .format (
562
+ artifact_type = artifact_type
563
+ )
564
+ )
568
565
569
566
# If all the conditions were met, the artifact is valid
570
567
valid_artifacts .append (artifact_type )
571
568
572
569
return valid_artifacts
573
570
574
571
def on_success (self , retval , task_id , args , kwargs ):
575
-
576
572
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
573
588
574
# NOTE: we are updating the db version instance *only* when
575
+ # TODO: remove this condition and *always* update the DB Version instance
589
576
if "html" in valid_artifacts :
590
577
try :
591
578
api_v2 .version (self .data .version .pk ).patch (
@@ -601,7 +588,8 @@ def on_success(self, retval, task_id, args, kwargs):
601
588
# NOTE: I think we should fail the build if we cannot update
602
589
# the version at this point. Otherwise, we will have inconsistent data
603
590
log .exception (
604
- 'Updating version failed, skipping file sync.' ,
591
+ "Updating version db object failed. "
592
+ 'Files are synced in the storage, but "Version" object is not updated' ,
605
593
)
606
594
607
595
# Index search data
@@ -698,10 +686,13 @@ def update_build(self, state=None):
698
686
try :
699
687
api_v2 .build (self .data .build ['id' ]).patch (self .data .build )
700
688
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' )
689
+ # NOTE: we are updating the "Build" object on each `state`.
690
+ # Only if the last update fails, there may be some inconsistency
691
+ # between the "Build" object in our db and the reality.
692
+ #
693
+ # The `state` argument will help us to track this more and understand
694
+ # at what state our updates are failing and decide what to do.
695
+ log .exception ("Error while updating the build object." , state = state )
705
696
706
697
def execute (self ):
707
698
self .data .build_director = BuildDirector (
@@ -749,6 +740,12 @@ def execute(self):
749
740
finally :
750
741
self .data .build_data = self .collect_build_data ()
751
742
743
+ # At this point, the user's build already succeeded.
744
+ # However, we cannot use `.on_success()` because we still have to upload the artifacts;
745
+ # which could fail, and we want to detect that and handle it properly at `.on_failure()`
746
+ # Store build artifacts to storage (local or cloud storage)
747
+ self .store_build_artifacts ()
748
+
752
749
def collect_build_data (self ):
753
750
"""
754
751
Collect data from the current build.
@@ -817,7 +814,7 @@ def set_valid_clone(self):
817
814
self .data .project .has_valid_clone = True
818
815
self .data .version .project .has_valid_clone = True
819
816
820
- def store_build_artifacts (self , artifacts ):
817
+ def store_build_artifacts (self ):
821
818
"""
822
819
Save build artifacts to "storage" using Django's storage API.
823
820
@@ -826,17 +823,16 @@ def store_build_artifacts(self, artifacts):
826
823
827
824
Remove build artifacts of types not included in this build (PDF, ePub, zip only).
828
825
"""
826
+ time_before_store_build_artifacts = timezone .now ()
829
827
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
828
self .update_build (state = BUILD_STATE_UPLOADING )
834
829
830
+ valid_artifacts = self .get_valid_artifact_types ()
835
831
types_to_copy = []
836
832
types_to_delete = []
837
833
838
834
for artifact_type in ARTIFACT_TYPES :
839
- if artifact_type in artifacts :
835
+ if artifact_type in valid_artifacts :
840
836
types_to_copy .append (artifact_type )
841
837
# Never delete HTML nor JSON (search index)
842
838
elif artifact_type not in UNDELETABLE_ARTIFACT_TYPES :
@@ -857,14 +853,20 @@ def store_build_artifacts(self, artifacts):
857
853
try :
858
854
build_media_storage .sync_directory (from_path , to_path )
859
855
except Exception :
860
- # Ideally this should just be an IOError
861
- # but some storage backends unfortunately throw other errors
856
+ # NOTE: the exceptions reported so far are:
857
+ # - botocore.exceptions:HTTPClientError
858
+ # - botocore.exceptions:ClientError
859
+ # - readthedocs.doc_builder.exceptions:BuildCancelled
862
860
log .exception (
863
- ' Error copying to storage (not failing build)' ,
861
+ " Error copying to storage" ,
864
862
media_type = media_type ,
865
863
from_path = from_path ,
866
864
to_path = to_path ,
867
865
)
866
+ # Re-raise the exception to fail the build and handle it
867
+ # automatically at `on_failure`.
868
+ # It will clearly communicate the error to the user.
869
+ raise BuildAppError ("Error uploading files to the storage." )
868
870
869
871
# Delete formats
870
872
for media_type in types_to_delete :
@@ -877,13 +879,21 @@ def store_build_artifacts(self, artifacts):
877
879
try :
878
880
build_media_storage .delete_directory (media_path )
879
881
except Exception :
880
- # Ideally this should just be an IOError
881
- # but some storage backends unfortunately throw other errors
882
+ # NOTE: I didn't find any log line for this case yet
882
883
log .exception (
883
- ' Error deleting from storage (not failing build)' ,
884
+ " Error deleting files from storage" ,
884
885
media_type = media_type ,
885
886
media_path = media_path ,
886
887
)
888
+ # Re-raise the exception to fail the build and handle it
889
+ # automatically at `on_failure`.
890
+ # It will clearly communicate the error to the user.
891
+ raise BuildAppError ("Error deleting files from storage." )
892
+
893
+ log .info (
894
+ "Store build artifacts finished." ,
895
+ time = (timezone .now () - time_before_store_build_artifacts ).seconds ,
896
+ )
887
897
888
898
def send_notifications (self , version_pk , build_pk , event ):
889
899
"""Send notifications to all subscribers of `event`."""
0 commit comments