files.com 1.1.813 → 1.1.815
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/_VERSION +1 -1
- data/doc/Files/APIConnectionError.html +1 -1
- data/doc/Files/APIError.html +1 -1
- data/doc/Files/AccountAlreadyExistsError.html +1 -1
- data/doc/Files/AccountLineItem.html +1 -1
- data/doc/Files/AccountOverdueError.html +1 -1
- data/doc/Files/Action.html +1 -1
- data/doc/Files/ActionLog.html +1 -1
- data/doc/Files/ActionNotificationExport.html +1 -1
- data/doc/Files/ActionNotificationExportResult.html +1 -1
- data/doc/Files/AdditionalAuthenticationRequiredError.html +1 -1
- data/doc/Files/AgentNode.html +1 -1
- data/doc/Files/AgentNodeConnection.html +1 -1
- data/doc/Files/AgentNodeInstance.html +1 -1
- data/doc/Files/AgentPushUpdate.html +1 -1
- data/doc/Files/AgentPushUpdateBlockedError.html +1 -1
- data/doc/Files/AgentUnavailableError.html +1 -1
- data/doc/Files/AgentUpgradeRequiredError.html +1 -1
- data/doc/Files/AiAssistantPersonality.html +1 -1
- data/doc/Files/AiTask.html +1 -1
- data/doc/Files/AiTaskCannotBeRunManuallyError.html +1 -1
- data/doc/Files/AlreadyCompletedError.html +1 -1
- data/doc/Files/Api.html +20 -13
- data/doc/Files/ApiClient/RequestLogContext.html +29 -29
- data/doc/Files/ApiClient/TransferSettings.html +721 -0
- data/doc/Files/ApiClient.html +756 -204
- data/doc/Files/ApiKey.html +1 -1
- data/doc/Files/ApiKeyIsDisabledError.html +1 -1
- data/doc/Files/ApiKeyIsPathRestrictedError.html +1 -1
- data/doc/Files/ApiKeyNotFoundError.html +1 -1
- data/doc/Files/ApiKeyOnlyForDesktopAppError.html +1 -1
- data/doc/Files/ApiKeyOnlyForFileOperationsError.html +1 -1
- data/doc/Files/ApiKeyOnlyForMobileAppError.html +1 -1
- data/doc/Files/ApiKeyOnlyForOfficeIntegrationError.html +1 -1
- data/doc/Files/ApiKeySessionsNotSupportedError.html +1 -1
- data/doc/Files/ApiRequestLog.html +1 -1
- data/doc/Files/App.html +1 -1
- data/doc/Files/As2IncomingMessage.html +1 -1
- data/doc/Files/As2OutgoingMessage.html +1 -1
- data/doc/Files/As2Partner.html +1 -1
- data/doc/Files/As2Station.html +1 -1
- data/doc/Files/AttachmentTooLargeError.html +1 -1
- data/doc/Files/AuthenticationError.html +1 -1
- data/doc/Files/AuthenticationRequiredError.html +1 -1
- data/doc/Files/Auto.html +1 -1
- data/doc/Files/Automation.html +1 -1
- data/doc/Files/AutomationAuthoringSchema.html +1 -1
- data/doc/Files/AutomationCannotBeRunManuallyError.html +1 -1
- data/doc/Files/AutomationExecutionNode.html +1 -1
- data/doc/Files/AutomationLog.html +1 -1
- data/doc/Files/AutomationRun.html +1 -1
- data/doc/Files/AutomationsUnavailableError.html +1 -1
- data/doc/Files/BadRequestError.html +1 -1
- data/doc/Files/BandwidthSnapshot.html +1 -1
- data/doc/Files/Behavior.html +1 -1
- data/doc/Files/BehaviorNotAllowedOnRemoteServerError.html +1 -1
- data/doc/Files/BillingInformationHiddenError.html +1 -1
- data/doc/Files/BillingPermissionRequiredError.html +1 -1
- data/doc/Files/BufferedUploadDisabledForThisDestinationError.html +1 -1
- data/doc/Files/Bundle.html +1 -1
- data/doc/Files/BundleAction.html +1 -1
- data/doc/Files/BundleDownload.html +1 -1
- data/doc/Files/BundleMaximumUsesReachedError.html +1 -1
- data/doc/Files/BundleNotification.html +1 -1
- data/doc/Files/BundleOnlyAllowsPreviewsError.html +1 -1
- data/doc/Files/BundleOperationRequiresSubfolderError.html +1 -1
- data/doc/Files/BundlePath.html +1 -1
- data/doc/Files/BundlePathNotFoundError.html +1 -1
- data/doc/Files/BundlePermissionRequiredError.html +1 -1
- data/doc/Files/BundleRecipient.html +1 -1
- data/doc/Files/BundleRegistration.html +1 -1
- data/doc/Files/BundleRegistrationCodeFailedError.html +1 -1
- data/doc/Files/BundleRegistrationNotFoundError.html +1 -1
- data/doc/Files/CannotAdministerHigherLevelUserError.html +1 -1
- data/doc/Files/CannotDownloadDirectoryError.html +1 -1
- data/doc/Files/CannotLoginWhileUsingKeyError.html +1 -1
- data/doc/Files/CantActForOtherUserError.html +1 -1
- data/doc/Files/CantMoveWithMultipleLocationsError.html +1 -1
- data/doc/Files/ChatMessage.html +1 -1
- data/doc/Files/ChatSession.html +1 -1
- data/doc/Files/ChildSiteManagementPolicy.html +1 -1
- data/doc/Files/Clickwrap.html +1 -1
- data/doc/Files/CodeNotFoundError.html +1 -1
- data/doc/Files/ConfigurationLockedPathError.html +1 -1
- data/doc/Files/ContactAdminForPasswordChangeHelpError.html +1 -1
- data/doc/Files/CouldNotCreateParentError.html +1 -1
- data/doc/Files/CustomDomain.html +1 -1
- data/doc/Files/DatetimeParseError.html +1 -1
- data/doc/Files/DesktopConfigurationProfile.html +1 -1
- data/doc/Files/DestinationExistsError.html +1 -1
- data/doc/Files/DestinationFolderLimitedError.html +1 -1
- data/doc/Files/DestinationParentConflictError.html +1 -1
- data/doc/Files/DestinationParentDoesNotExistError.html +1 -1
- data/doc/Files/DestinationSameError.html +1 -1
- data/doc/Files/DestinationSiteMismatchError.html +1 -1
- data/doc/Files/DirectConnectionInfo.html +1 -1
- data/doc/Files/DnsRecord.html +1 -1
- data/doc/Files/DoesNotSupportSortingError.html +1 -1
- data/doc/Files/DuplicateShareRecipientError.html +1 -1
- data/doc/Files/EmailIncomingMessage.html +1 -1
- data/doc/Files/EmailLog.html +1 -1
- data/doc/Files/Error.html +2 -2
- data/doc/Files/Errors.html +1 -1
- data/doc/Files/EventChannel.html +1 -1
- data/doc/Files/EventDeliveryAttempt.html +1 -1
- data/doc/Files/EventRecord.html +1 -1
- data/doc/Files/EventSubscription.html +1 -1
- data/doc/Files/EventTarget.html +1 -1
- data/doc/Files/ExavaultApiRequestLog.html +1 -1
- data/doc/Files/ExceededRuntimeLimitError.html +1 -1
- data/doc/Files/Expectation.html +1 -1
- data/doc/Files/ExpectationAlreadyHasOpenWindowError.html +1 -1
- data/doc/Files/ExpectationEvaluation.html +1 -1
- data/doc/Files/ExpectationIncident.html +1 -1
- data/doc/Files/ExpectationNotManualTriggerError.html +1 -1
- data/doc/Files/ExpiredPrivateKeyError.html +1 -1
- data/doc/Files/ExpiredPublicKeyError.html +1 -1
- data/doc/Files/ExportFailureError.html +1 -1
- data/doc/Files/ExportNotReadyError.html +1 -1
- data/doc/Files/ExternalEvent.html +1 -1
- data/doc/Files/FailedToChangePasswordError.html +1 -1
- data/doc/Files/File.html +2019 -1515
- data/doc/Files/FileAction.html +1 -1
- data/doc/Files/FileActionError.html +1 -1
- data/doc/Files/FileComment.html +1 -1
- data/doc/Files/FileCommentReaction.html +1 -1
- data/doc/Files/FileLockedError.html +1 -1
- data/doc/Files/FileMigration.html +1 -1
- data/doc/Files/FileMigrationLog.html +1 -1
- data/doc/Files/FileNotFoundError.html +1 -1
- data/doc/Files/FileNotUploadedError.html +1 -1
- data/doc/Files/FilePendingProcessingError.html +1 -1
- data/doc/Files/FileProcessingErrorError.html +1 -1
- data/doc/Files/FileTooBigToDecryptError.html +1 -1
- data/doc/Files/FileTooBigToEncryptError.html +1 -1
- data/doc/Files/FileUploadNotFoundError.html +1 -1
- data/doc/Files/FileUploadPart.html +1 -1
- data/doc/Files/FileUploadedToWrongRegionError.html +1 -1
- data/doc/Files/FileUtils.html +1 -1
- data/doc/Files/FilenameTooLongError.html +1 -1
- data/doc/Files/FilesAgentFailedAuthorizationError.html +1 -1
- data/doc/Files/FilesNativeStorageOverloadedError.html +1 -1
- data/doc/Files/Folder.html +1 -1
- data/doc/Files/FolderAdminOrBillingPermissionRequiredError.html +1 -1
- data/doc/Files/FolderAdminPermissionRequiredError.html +1 -1
- data/doc/Files/FolderLockedError.html +1 -1
- data/doc/Files/FolderMustNotBeAFileError.html +1 -1
- data/doc/Files/FolderNotEmptyError.html +1 -1
- data/doc/Files/FoldersNotAllowedError.html +1 -1
- data/doc/Files/FormField.html +1 -1
- data/doc/Files/FormFieldSet.html +1 -1
- data/doc/Files/FtpActionLog.html +1 -1
- data/doc/Files/FullPermissionRequiredError.html +1 -1
- data/doc/Files/GpgKey.html +1 -1
- data/doc/Files/Group.html +1 -1
- data/doc/Files/GroupNotFoundError.html +1 -1
- data/doc/Files/GroupUser.html +1 -1
- data/doc/Files/History.html +1 -1
- data/doc/Files/HistoryExport.html +1 -1
- data/doc/Files/HistoryExportResult.html +1 -1
- data/doc/Files/HistoryPermissionRequiredError.html +1 -1
- data/doc/Files/HistoryUnavailableError.html +1 -1
- data/doc/Files/HolidayCalendar.html +1 -1
- data/doc/Files/HolidayRegion.html +1 -1
- data/doc/Files/Image.html +1 -1
- data/doc/Files/InAppAiAssistantUnavailableError.html +1 -1
- data/doc/Files/InboundS3Log.html +1 -1
- data/doc/Files/InboxNotFoundError.html +1 -1
- data/doc/Files/InboxRecipient.html +1 -1
- data/doc/Files/InboxRegistration.html +1 -1
- data/doc/Files/InboxRegistrationCodeFailedError.html +1 -1
- data/doc/Files/InboxUpload.html +1 -1
- data/doc/Files/InsufficientPermissionForParamsError.html +1 -1
- data/doc/Files/InsufficientPermissionForSiteError.html +1 -1
- data/doc/Files/IntegrationCentricProfile.html +1 -1
- data/doc/Files/InternalGeneralErrorError.html +1 -1
- data/doc/Files/InvalidBodyError.html +1 -1
- data/doc/Files/InvalidBundleCodeError.html +1 -1
- data/doc/Files/InvalidCredentialsError.html +1 -1
- data/doc/Files/InvalidCursorError.html +1 -1
- data/doc/Files/InvalidCursorTypeForSortError.html +1 -1
- data/doc/Files/InvalidEtagsError.html +1 -1
- data/doc/Files/InvalidFileTypeError.html +1 -1
- data/doc/Files/InvalidFilenameError.html +1 -1
- data/doc/Files/InvalidFilterAliasCombinationError.html +1 -1
- data/doc/Files/InvalidFilterFieldError.html +1 -1
- data/doc/Files/InvalidFilterParamError.html +1 -1
- data/doc/Files/InvalidFilterParamFormatError.html +1 -1
- data/doc/Files/InvalidFilterParamValueError.html +1 -1
- data/doc/Files/InvalidInputEncodingError.html +1 -1
- data/doc/Files/InvalidInterfaceError.html +1 -1
- data/doc/Files/InvalidOauthError.html +1 -1
- data/doc/Files/InvalidOauthProviderError.html +1 -1
- data/doc/Files/InvalidOrExpiredCodeError.html +1 -1
- data/doc/Files/InvalidParameterError.html +5 -1
- data/doc/Files/InvalidPathError.html +1 -1
- data/doc/Files/InvalidPriorityColorError.html +1 -1
- data/doc/Files/InvalidRangeError.html +1 -1
- data/doc/Files/InvalidReturnToUrlError.html +1 -1
- data/doc/Files/InvalidSearchQueryError.html +1 -1
- data/doc/Files/InvalidSessionError.html +1 -1
- data/doc/Files/InvalidSiteError.html +1 -1
- data/doc/Files/InvalidSortFieldError.html +1 -1
- data/doc/Files/InvalidSortFilterCombinationError.html +1 -1
- data/doc/Files/InvalidUploadOffsetError.html +1 -1
- data/doc/Files/InvalidUploadPartGapError.html +1 -1
- data/doc/Files/InvalidUploadPartSizeError.html +1 -1
- data/doc/Files/InvalidUsernameOrPasswordError.html +1 -1
- data/doc/Files/InvalidWorkspaceIdHeaderError.html +1 -1
- data/doc/Files/InvalidZipFileError.html +1 -1
- data/doc/Files/Invoice.html +1 -1
- data/doc/Files/InvoiceLineItem.html +1 -1
- data/doc/Files/IpAddress.html +1 -1
- data/doc/Files/KeyLifecycleRule.html +1 -1
- data/doc/Files/List.html +1 -1
- data/doc/Files/Lock.html +1 -1
- data/doc/Files/LockOperationBusyError.html +1 -1
- data/doc/Files/LockedOutError.html +1 -1
- data/doc/Files/LockoutRegionMismatchError.html +1 -1
- data/doc/Files/MetadataCategory.html +1 -1
- data/doc/Files/MetadataNotSupportedOnRemotesError.html +1 -1
- data/doc/Files/MethodNotAllowedError.html +1 -1
- data/doc/Files/MigrationInProgressError.html +1 -1
- data/doc/Files/MissingParameterError.html +1 -1
- data/doc/Files/ModelSaveErrorError.html +1 -1
- data/doc/Files/MoverAccessDeniedError.html +1 -1
- data/doc/Files/MoverPackageRequiredError.html +1 -1
- data/doc/Files/MultipleProcessingErrorsError.html +1 -1
- data/doc/Files/MultipleSortParamsNotAllowedError.html +1 -1
- data/doc/Files/MustAuthenticateWithApiKeyError.html +1 -1
- data/doc/Files/NeedAdminPermissionForInboxError.html +1 -1
- data/doc/Files/NestedNotFoundError.html +1 -1
- data/doc/Files/NoAccountForSiteError.html +1 -1
- data/doc/Files/NoValidInputParamsError.html +1 -1
- data/doc/Files/NonAdminsMustQueryByFolderOrPathError.html +1 -1
- data/doc/Files/NotAllowedToCreateBundleError.html +1 -1
- data/doc/Files/NotAuthenticatedError.html +1 -1
- data/doc/Files/NotAuthorizedError.html +1 -1
- data/doc/Files/NotEnqueuableSyncError.html +1 -1
- data/doc/Files/NotFoundError.html +1 -1
- data/doc/Files/NotImplementedError.html +1 -1
- data/doc/Files/Notification.html +1 -1
- data/doc/Files/OffsetUploadNotAllowedWithMalwareScanningError.html +1 -1
- data/doc/Files/OneTimePasswordIncorrectError.html +1 -1
- data/doc/Files/OutboundConnectionLog.html +1 -1
- data/doc/Files/PartNumberTooLargeError.html +1 -1
- data/doc/Files/Partner.html +1 -1
- data/doc/Files/PartnerChannel.html +1 -1
- data/doc/Files/PartnerChannelTemplate.html +1 -1
- data/doc/Files/PartnerConnection.html +1 -1
- data/doc/Files/PartnerSite.html +1 -1
- data/doc/Files/PartnerSiteRequest.html +1 -1
- data/doc/Files/PasswordChangeNotRequiredError.html +1 -1
- data/doc/Files/PasswordChangeRequiredError.html +1 -1
- data/doc/Files/PathCannotHaveTrailingWhitespaceError.html +1 -1
- data/doc/Files/PathTooLongError.html +1 -1
- data/doc/Files/PathUtil.html +1 -1
- data/doc/Files/Payment.html +1 -1
- data/doc/Files/PaymentLineItem.html +1 -1
- data/doc/Files/PaymentMethodErrorError.html +1 -1
- data/doc/Files/PendingWorkEvent.html +1 -1
- data/doc/Files/Permission.html +1 -1
- data/doc/Files/PlanNotFoundError.html +1 -1
- data/doc/Files/Preview.html +1 -1
- data/doc/Files/PreviewOnlyPermissionCannotDownloadError.html +1 -1
- data/doc/Files/ProcessingFailureError.html +1 -1
- data/doc/Files/PublicHostingRequestLog.html +1 -1
- data/doc/Files/PublicIpAddress.html +1 -1
- data/doc/Files/PublicKey.html +1 -1
- data/doc/Files/RateLimitedError.html +1 -1
- data/doc/Files/ReadOnlySessionError.html +1 -1
- data/doc/Files/ReadPermissionRequiredError.html +1 -1
- data/doc/Files/ReauthenticationFailedError.html +1 -1
- data/doc/Files/ReauthenticationFailedFinalError.html +1 -1
- data/doc/Files/ReauthenticationNeededActionError.html +1 -1
- data/doc/Files/ReauthenticationNeededFieldsError.html +1 -1
- data/doc/Files/ReauthenticationRateLimitedError.html +1 -1
- data/doc/Files/RecaptchaFailedError.html +1 -1
- data/doc/Files/RecipientAlreadySharedError.html +1 -1
- data/doc/Files/RemoteBandwidthSnapshot.html +1 -1
- data/doc/Files/RemoteDesktopDebugLoggingDisabledError.html +1 -1
- data/doc/Files/RemoteEntryReadOnlyError.html +1 -1
- data/doc/Files/RemoteMountBackend.html +1 -1
- data/doc/Files/RemoteServer.html +1 -1
- data/doc/Files/RemoteServerConfigurationFile.html +1 -1
- data/doc/Files/RemoteServerCredential.html +1 -1
- data/doc/Files/RemoteServerErrorError.html +1 -1
- data/doc/Files/Request.html +1 -1
- data/doc/Files/RequestBodyTooLargeError.html +1 -1
- data/doc/Files/RequestParamsContainInvalidCharacterError.html +1 -1
- data/doc/Files/RequestParamsInvalidError.html +1 -1
- data/doc/Files/RequestParamsRequiredError.html +1 -1
- data/doc/Files/ResourceBelongsToParentSiteError.html +1 -1
- data/doc/Files/ResourceLockedError.html +1 -1
- data/doc/Files/Response.html +1 -1
- data/doc/Files/Restore.html +1 -1
- data/doc/Files/RootFolderBehaviorSiteAdminRequiredError.html +1 -1
- data/doc/Files/RootFolderBehaviorSkipSiteAdminRequiredError.html +1 -1
- data/doc/Files/Schedule.html +1 -1
- data/doc/Files/ScheduledExport.html +1 -1
- data/doc/Files/ScimLog.html +1 -1
- data/doc/Files/SearchAllOnChildPathError.html +1 -1
- data/doc/Files/SearchUnavailableError.html +1 -1
- data/doc/Files/Secret.html +1 -1
- data/doc/Files/SelfManagedRequiredError.html +1 -1
- data/doc/Files/ServiceUnavailableError.html +1 -1
- data/doc/Files/Session.html +1 -1
- data/doc/Files/SettingsChange.html +1 -1
- data/doc/Files/SftpActionLog.html +1 -1
- data/doc/Files/SftpHostKey.html +1 -1
- data/doc/Files/ShareGroup.html +1 -1
- data/doc/Files/ShareGroupMember.html +1 -1
- data/doc/Files/SiemHttpDestination.html +1 -1
- data/doc/Files/SiemHttpDestinationEvent.html +1 -1
- data/doc/Files/Site.html +911 -731
- data/doc/Files/SiteAdminOrPartnerAdminPermissionRequiredError.html +1 -1
- data/doc/Files/SiteAdminOrWorkspaceAdminOrFolderAdminPermissionRequiredError.html +1 -1
- data/doc/Files/SiteAdminOrWorkspaceAdminOrPartnerAdminOrFolderAdminPermissionRequiredError.html +1 -1
- data/doc/Files/SiteAdminOrWorkspaceAdminOrPartnerAdminPermissionRequiredError.html +1 -1
- data/doc/Files/SiteAdminOrWorkspaceAdminPermissionRequiredError.html +1 -1
- data/doc/Files/SiteAdminRequiredError.html +1 -1
- data/doc/Files/SiteConfigurationError.html +1 -1
- data/doc/Files/SiteDisabledError.html +1 -1
- data/doc/Files/SiteFilesAreImmutableError.html +1 -1
- data/doc/Files/SiteNotFoundError.html +1 -1
- data/doc/Files/SiteSubdomainRedirect.html +1 -1
- data/doc/Files/SiteWasRemovedError.html +1 -1
- data/doc/Files/SizableIO.html +1 -1
- data/doc/Files/Snapshot.html +1 -1
- data/doc/Files/SsoEvent.html +1 -1
- data/doc/Files/SsoStrategy.html +1 -1
- data/doc/Files/Status.html +1 -1
- data/doc/Files/Style.html +1 -1
- data/doc/Files/SubfolderLockedError.html +1 -1
- data/doc/Files/Sync.html +1 -1
- data/doc/Files/SyncInProgressError.html +1 -1
- data/doc/Files/SyncLog.html +1 -1
- data/doc/Files/SyncRun.html +1 -1
- data/doc/Files/SyncRunLiveTransfer.html +1 -1
- data/doc/Files/SystemProfiler.html +1 -1
- data/doc/Files/TooManyConcurrentLoginsError.html +1 -1
- data/doc/Files/TooManyConcurrentRequestsError.html +1 -1
- data/doc/Files/TooManyLoginAttemptsError.html +1 -1
- data/doc/Files/TooManyRequestsError.html +1 -1
- data/doc/Files/TooManySharesError.html +1 -1
- data/doc/Files/Transfer/BufferLimitError.html +175 -0
- data/doc/Files/Transfer/ByteBudget/Reservation.html +301 -0
- data/doc/Files/Transfer/ByteBudget.html +748 -0
- data/doc/Files/Transfer/CancelledError.html +235 -0
- data/doc/Files/Transfer/Checkpoint.html +1371 -0
- data/doc/Files/Transfer/CheckpointError.html +173 -0
- data/doc/Files/Transfer/ClientTransport/ClientResponse.html +661 -0
- data/doc/Files/Transfer/ClientTransport/Reader.html +530 -0
- data/doc/Files/Transfer/ClientTransport.html +675 -0
- data/doc/Files/Transfer/Clock.html +189 -0
- data/doc/Files/Transfer/Control.html +667 -0
- data/doc/Files/Transfer/Controller/Attempt.html +465 -0
- data/doc/Files/Transfer/Controller.html +1166 -0
- data/doc/Files/Transfer/Coordinator/Timing.html +568 -0
- data/doc/Files/Transfer/Coordinator.html +534 -0
- data/doc/Files/Transfer/DownloadCoordinator/FileSink.html +976 -0
- data/doc/Files/Transfer/DownloadCoordinator/IoSink.html +1168 -0
- data/doc/Files/Transfer/DownloadCoordinator/Piece.html +875 -0
- data/doc/Files/Transfer/DownloadCoordinator.html +728 -0
- data/doc/Files/Transfer/FileSlots.html +480 -0
- data/doc/Files/Transfer/FrontendResponseError.html +235 -0
- data/doc/Files/Transfer/Holds.html +505 -0
- data/doc/Files/Transfer/HttpTransport.html +579 -0
- data/doc/Files/Transfer/Job.html +2338 -0
- data/doc/Files/Transfer/Options.html +924 -0
- data/doc/Files/Transfer/PausedError.html +319 -0
- data/doc/Files/Transfer/Policy.html +1642 -0
- data/doc/Files/Transfer/RequestPermits.html +160 -0
- data/doc/Files/Transfer/SourceChangedError.html +235 -0
- data/doc/Files/Transfer/StorageResponse.html +546 -0
- data/doc/Files/Transfer/StorageRetry.html +1098 -0
- data/doc/Files/Transfer/UploadCoordinator/Grant.html +1048 -0
- data/doc/Files/Transfer/UploadCoordinator/Part.html +1048 -0
- data/doc/Files/Transfer/UploadCoordinator/Region.html +295 -0
- data/doc/Files/Transfer/UploadCoordinator/Segments.html +299 -0
- data/doc/Files/Transfer/UploadCoordinator/Source.html +725 -0
- data/doc/Files/Transfer/UploadCoordinator/StreamSource.html +664 -0
- data/doc/Files/Transfer/UploadCoordinator.html +546 -0
- data/doc/Files/Transfer/UploadPlan.html +522 -0
- data/doc/Files/Transfer/VersionChangedError.html +235 -0
- data/doc/Files/Transfer.html +134 -0
- data/doc/Files/TrialExpiredError.html +1 -1
- data/doc/Files/TrialLockedError.html +1 -1
- data/doc/Files/TwoFactorAuthenticationCodeAlreadySentError.html +1 -1
- data/doc/Files/TwoFactorAuthenticationCountryBlacklistedError.html +1 -1
- data/doc/Files/TwoFactorAuthenticationErrorError.html +1 -1
- data/doc/Files/TwoFactorAuthenticationGeneralErrorError.html +1 -1
- data/doc/Files/TwoFactorAuthenticationMethodUnsupportedErrorError.html +1 -1
- data/doc/Files/TwoFactorAuthenticationRequiredError.html +1 -1
- data/doc/Files/TwoFactorAuthenticationSetupExpiredError.html +1 -1
- data/doc/Files/TwoFactorAuthenticationUnsubscribedRecipientError.html +1 -1
- data/doc/Files/URI.html +1 -1
- data/doc/Files/UnrecognizedSortIndexError.html +1 -1
- data/doc/Files/UnsupportedCurrencyError.html +1 -1
- data/doc/Files/UnsupportedHttpResponseFormatError.html +1 -1
- data/doc/Files/UnsupportedMediaTypeError.html +1 -1
- data/doc/Files/UpdatesNotAllowedForRemotesError.html +1 -1
- data/doc/Files/UploadsUnavailableError.html +1 -1
- data/doc/Files/UsageByTopLevelDir.html +1 -1
- data/doc/Files/UsageDailySnapshot.html +1 -1
- data/doc/Files/UsageSnapshot.html +1 -1
- data/doc/Files/User.html +597 -386
- data/doc/Files/UserAdditionalEmailRecipient.html +1 -1
- data/doc/Files/UserCipherUse.html +1 -1
- data/doc/Files/UserIdInvalidError.html +1 -1
- data/doc/Files/UserIdOnUserEndpointError.html +1 -1
- data/doc/Files/UserIdWithoutSiteAdminError.html +1 -1
- data/doc/Files/UserLifecycleRule.html +1 -1
- data/doc/Files/UserNotFoundError.html +1 -1
- data/doc/Files/UserRequest.html +1 -1
- data/doc/Files/UserRequestsEnabledRequiredError.html +1 -1
- data/doc/Files/UserRequiredError.html +1 -1
- data/doc/Files/UserSecurityEvent.html +1 -1
- data/doc/Files/UserSftpClientUse.html +1 -1
- data/doc/Files/Util/LogSettings.html +1 -1
- data/doc/Files/Util.html +1 -1
- data/doc/Files/WebDavActionLog.html +1 -1
- data/doc/Files/WebhookTest.html +1 -1
- data/doc/Files/Workspace.html +1 -1
- data/doc/Files/WriteAndBundlePermissionRequiredError.html +1 -1
- data/doc/Files/WritePermissionRequiredError.html +1 -1
- data/doc/Files/ZipListEntry.html +1 -1
- data/doc/Files.html +116 -116
- data/doc/_index.html +314 -4
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +1 -1
- data/doc/index.html +1 -1
- data/doc/method_list.html +5368 -3048
- data/doc/top-level-namespace.html +1 -1
- data/docs/file.md +346 -0
- data/docs/site.md +10 -0
- data/docs/user.md +8 -0
- data/lib/files.com/api.rb +6 -1
- data/lib/files.com/api_client.rb +220 -32
- data/lib/files.com/models/file.rb +124 -17
- data/lib/files.com/models/site.rb +16 -0
- data/lib/files.com/models/user.rb +19 -0
- data/lib/files.com/transfer/budgets.rb +123 -0
- data/lib/files.com/transfer/checkpoint.rb +113 -0
- data/lib/files.com/transfer/clock.rb +14 -0
- data/lib/files.com/transfer/controller.rb +549 -0
- data/lib/files.com/transfer/download_coordinator.rb +906 -0
- data/lib/files.com/transfer/errors.rb +100 -0
- data/lib/files.com/transfer/http_transport.rb +337 -0
- data/lib/files.com/transfer/job.rb +778 -0
- data/lib/files.com/transfer/options.rb +126 -0
- data/lib/files.com/transfer/policy.rb +91 -0
- data/lib/files.com/transfer/storage_retry.rb +169 -0
- data/lib/files.com/transfer/upload_coordinator.rb +657 -0
- data/lib/files.com/transfer/upload_plan.rb +157 -0
- data/lib/files.com.rb +13 -0
- data/spec/lib/api_client_spec.rb +125 -3
- data/spec/models/file_transfer_spec.rb +53 -4
- data/spec/support/transfer_server.rb +113 -14
- data/spec/transfer/adaptive_download_spec.rb +1102 -0
- data/spec/transfer/adaptive_upload_spec.rb +1602 -0
- data/spec/transfer/concurrency_controller_spec.rb +382 -0
- data/spec/workspace_id_spec.rb +19 -14
- metadata +59 -1
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Files
|
|
4
|
+
module Transfer
|
|
5
|
+
# Decides how many requests to one destination may be in flight at once, from those requests' own completions.
|
|
6
|
+
#
|
|
7
|
+
# It measures in windows. A window closes once it has accepted at least max(window attempts, limit) attempts and
|
|
8
|
+
# lasted at least their mean duration, and is judged only when its requests on the network averaged at least 75% of
|
|
9
|
+
# the limit, so a transfer short of work, or waiting on the API, its source, admission or memory, teaches it
|
|
10
|
+
# nothing. A window's throughput is its accepted bytes over its wall time; its latency L is its attempts' summed
|
|
11
|
+
# durations over those bytes; Lmin is the lowest L of any window; and with I requests on the network on average,
|
|
12
|
+
# its queue estimate is Q = I * (1 - Lmin / L). An accepted attempt counts in the window it finishes in, whichever
|
|
13
|
+
# epoch it was sent in, except during a throttle's cooldown.
|
|
14
|
+
#
|
|
15
|
+
# A policy with a warm start begins below its start. Its first judged window warms the connection and its next sets
|
|
16
|
+
# the baseline. From a baseline that is not already queued it probes upward, adding ceil(n / 2) to n, or 1 after a
|
|
17
|
+
# rest. The attempts in flight once the raised target first fills are the probe's cohort, known by their sequence
|
|
18
|
+
# numbers; the probe is judged by the first window opened after the last of them ends, however it ended, or by the
|
|
19
|
+
# window then closing once max(window attempts, target) answers have arrived since the raise, whichever comes
|
|
20
|
+
# first. It is kept when that window's throughput reaches the baseline's times 1 + 0.5 * d / n and its warm queue
|
|
21
|
+
# estimate, I * (1 - reference median / its median), stays under d / 2, and the next probe follows. Medians are of
|
|
22
|
+
# each accepted attempt's duration per byte, so windows whose attempts carry different sizes compare alike, and the
|
|
23
|
+
# reference median is the fastest judged at the target the probe came from (at a baseline's own target, for a
|
|
24
|
+
# baseline). A rejected probe returns to n and rests HOLD_WINDOWS + GROWTH_INTERVAL_WINDOWS judged windows of
|
|
25
|
+
# ordinary size, as a queued baseline does; the last of them is the next baseline. Warming goes on so below the
|
|
26
|
+
# start: it ends there, or at once on a judged window of 96 MiB/s or more. A failure or throttle in any epoch
|
|
27
|
+
# abandons the probe and starts the rest again, at whatever target its shrink left, down to one. The warming window
|
|
28
|
+
# and the windows before a cohort settles close after target answers; a baseline's or a probe's window after the
|
|
29
|
+
# answers the sampling rule below asks for, though one at 96 MiB/s closes after target answers.
|
|
30
|
+
#
|
|
31
|
+
# After that, each judged window grows the limit by the growth step (1 at or above soft) as a probe, keeps a probe
|
|
32
|
+
# whose window beats the one before by 1%, and gives it up after two windows that do not, holding a window. Q at
|
|
33
|
+
# queue_suppress stops growth, and at queue_backoff shrinks the limit by 8%, not below the latency floor.
|
|
34
|
+
#
|
|
35
|
+
# An attempt counts against the limit from when it is admitted (admit), but is on the network, and measured, only
|
|
36
|
+
# from when the transport starts sending it (sending) until it is answered (answered): an upload's attempt as its
|
|
37
|
+
# response headers arrive, a download's once its whole body has arrived and been checked. Waiting for a worker,
|
|
38
|
+
# reading a part's bytes before the send, writing to the destination and settling the outcome afterwards are not
|
|
39
|
+
# network time. Each attempt carries the epoch it was sent in, and every change of limit starts a new one. A failure
|
|
40
|
+
# shrinks the limit by 35% and a throttle by 10%, only for an attempt from the current epoch; every throttle also
|
|
41
|
+
# starts or extends a cooldown, during which completions are not measured, and restarts measurement, but only a
|
|
42
|
+
# change of limit starts a new epoch, so a throttle from an older one leaves the newer attempts' failures counting.
|
|
43
|
+
# A throttle's Retry-After holds its destination back through the job (Holds), whatever controller its requests
|
|
44
|
+
# use. A fixed policy never changes its limit. Its methods lock the controller, so any thread may call them.
|
|
45
|
+
class Controller
|
|
46
|
+
Attempt = Struct.new(:sequence, :epoch, :started_at, :ended_at)
|
|
47
|
+
MAX_DECISIONS = 256
|
|
48
|
+
MAX_SAMPLES = 1024
|
|
49
|
+
|
|
50
|
+
attr_reader :policy
|
|
51
|
+
|
|
52
|
+
def initialize(policy, clock: Clock)
|
|
53
|
+
@policy = policy
|
|
54
|
+
@clock = clock
|
|
55
|
+
@lock = Mutex.new
|
|
56
|
+
@limit = policy.initial
|
|
57
|
+
@phase = if policy.fixed?
|
|
58
|
+
:fixed
|
|
59
|
+
elsif policy.warm?
|
|
60
|
+
:warm
|
|
61
|
+
else
|
|
62
|
+
:ordinary
|
|
63
|
+
end
|
|
64
|
+
# While warming: :warming, :baseline, :settling (a probe's cohort is still running), :probing or :resting.
|
|
65
|
+
@stage = :warming
|
|
66
|
+
@epoch = 0
|
|
67
|
+
@sequence = 0
|
|
68
|
+
@admitted = 0
|
|
69
|
+
@inflight = 0
|
|
70
|
+
# Sequence numbers of the attempts admitted and not yet finished.
|
|
71
|
+
@outstanding = {}
|
|
72
|
+
@cohort = nil
|
|
73
|
+
@answers = 0
|
|
74
|
+
@warm_spread = 0.0
|
|
75
|
+
@warm_degrees = 0
|
|
76
|
+
@warm_medians = {}
|
|
77
|
+
@cooldown_until = nil
|
|
78
|
+
@latency_min = nil
|
|
79
|
+
@decisions = []
|
|
80
|
+
@warm_target = 2
|
|
81
|
+
@warm_probe = nil
|
|
82
|
+
@rest_windows = 0
|
|
83
|
+
@rested = false
|
|
84
|
+
@probe = nil
|
|
85
|
+
@hold_windows = 0
|
|
86
|
+
@judged_windows = 0
|
|
87
|
+
@ineligible_windows = 0
|
|
88
|
+
now = @clock.now
|
|
89
|
+
reset_window(now)
|
|
90
|
+
record(now, :start, @limit)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def limit
|
|
94
|
+
@lock.synchronize { @limit }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Attempts on the network now: from the start of their send until they are answered (see answered).
|
|
98
|
+
def inflight
|
|
99
|
+
@lock.synchronize { @inflight }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Attempts admitted and not yet finished, sent or not: what the limit bounds.
|
|
103
|
+
def admitted
|
|
104
|
+
@lock.synchronize { @admitted }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def phase
|
|
108
|
+
@lock.synchronize { @phase }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Its decisions, oldest first and at most the last 256, each a frozen Hash of :at (its clock's seconds),
|
|
112
|
+
# :phase, :from, :to and :reason.
|
|
113
|
+
def decisions
|
|
114
|
+
@lock.synchronize { @decisions.dup }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Its policy, phase, limit, attempts admitted, attempts on the network (inflight) and epoch, with how many
|
|
118
|
+
# measurement windows it judged and how many it closed without judging because too few requests were on the
|
|
119
|
+
# network (judged_windows and ineligible_windows).
|
|
120
|
+
def snapshot
|
|
121
|
+
@lock.synchronize do
|
|
122
|
+
{ policy: @policy.name, phase: @phase, limit: @limit, admitted: @admitted, inflight: @inflight, epoch: @epoch,
|
|
123
|
+
judged_windows: @judged_windows, ineligible_windows: @ineligible_windows }
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Counts an attempt against the limit from now, before it is sent, with the next sequence number; sending,
|
|
128
|
+
# answered and finish_attempt take what it returns.
|
|
129
|
+
def admit
|
|
130
|
+
@lock.synchronize do
|
|
131
|
+
@admitted += 1
|
|
132
|
+
attempt = Attempt.new(@sequence += 1)
|
|
133
|
+
@outstanding[attempt.sequence] = true
|
|
134
|
+
note_cohort
|
|
135
|
+
attempt
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# As the transport starts sending attempt, connection setup included: it is in flight, in the current epoch, from
|
|
140
|
+
# now.
|
|
141
|
+
def sending(attempt)
|
|
142
|
+
@lock.synchronize do
|
|
143
|
+
return if attempt.started_at
|
|
144
|
+
|
|
145
|
+
now = @clock.now
|
|
146
|
+
advance(now)
|
|
147
|
+
@inflight += 1
|
|
148
|
+
attempt.epoch = @epoch
|
|
149
|
+
attempt.started_at = now
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# As attempt is answered, an upload's as its response headers arrive and a download's once its whole body has
|
|
154
|
+
# arrived and been checked: it is no longer on the network from now. Only the first call counts.
|
|
155
|
+
def answered(attempt)
|
|
156
|
+
@lock.synchronize { answer(attempt, @clock.now) }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Admits an attempt and starts sending it at once.
|
|
160
|
+
def start_attempt
|
|
161
|
+
attempt = admit
|
|
162
|
+
sending(attempt)
|
|
163
|
+
attempt
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# outcome is :success, with the bytes the attempt carried; :failure, for an error worth retrying; :throttle, for
|
|
167
|
+
# a 429 or 503, with its Retry-After seconds; or :ignored, for what says nothing about capacity, such as a
|
|
168
|
+
# cancellation, a refused or expired request or the caller's source or callback failing. An attempt never sent
|
|
169
|
+
# teaches nothing, but every attempt ending counts toward settling a probe's cohort. Returns the limit.
|
|
170
|
+
def finish_attempt(attempt, outcome, bytes: 0, retry_after: nil)
|
|
171
|
+
@lock.synchronize do
|
|
172
|
+
now = @clock.now
|
|
173
|
+
answer(attempt, now)
|
|
174
|
+
advance(now)
|
|
175
|
+
@admitted -= 1
|
|
176
|
+
@outstanding.delete(attempt.sequence)
|
|
177
|
+
if attempt.started_at
|
|
178
|
+
case outcome
|
|
179
|
+
when :success then measure(attempt, now, bytes)
|
|
180
|
+
when :failure then failed(attempt, now)
|
|
181
|
+
when :throttle then throttled(attempt, now, retry_after)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
settle_cohort(attempt, now)
|
|
185
|
+
@limit
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
def answer(attempt, now)
|
|
192
|
+
return unless attempt.started_at && attempt.ended_at.nil?
|
|
193
|
+
|
|
194
|
+
advance(now)
|
|
195
|
+
@inflight -= 1
|
|
196
|
+
attempt.ended_at = now
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Adds the time since the last event to the window's requests-in-flight integral. A window restarts when a
|
|
200
|
+
# cooldown ends, and nothing accumulates during one.
|
|
201
|
+
def advance(now)
|
|
202
|
+
if @cooldown_until
|
|
203
|
+
return if now < @cooldown_until
|
|
204
|
+
|
|
205
|
+
reset_window(@cooldown_until)
|
|
206
|
+
@cooldown_until = nil
|
|
207
|
+
end
|
|
208
|
+
@window[:busy] += @inflight * (now - @window[:last_change])
|
|
209
|
+
@window[:last_change] = now
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def reset_window(now)
|
|
213
|
+
@window = { started_at: now, last_change: now, busy: 0.0, attempts: 0, bytes: 0, durations: 0.0, samples: [] }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# An accepted attempt, in the current window whichever epoch it was sent in, unless a cooldown is running.
|
|
217
|
+
def measure(attempt, now, bytes)
|
|
218
|
+
return if @phase == :fixed || @cooldown_until
|
|
219
|
+
|
|
220
|
+
window = @window
|
|
221
|
+
duration = attempt.ended_at - attempt.started_at
|
|
222
|
+
window[:attempts] += 1
|
|
223
|
+
window[:bytes] += bytes
|
|
224
|
+
window[:durations] += duration
|
|
225
|
+
window[:samples] << (duration / bytes) if bytes.positive? && window[:samples].size < MAX_SAMPLES
|
|
226
|
+
answered_since_raise
|
|
227
|
+
elapsed = now - window[:started_at]
|
|
228
|
+
return unless elapsed >= window[:durations] / window[:attempts]
|
|
229
|
+
return unless window[:attempts] >= window_target || fast_window?(window, elapsed)
|
|
230
|
+
|
|
231
|
+
close_window(now)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# A warm window already at 96 MiB/s or more closes once target answers have arrived, without more samples.
|
|
235
|
+
def fast_window?(window, elapsed)
|
|
236
|
+
@phase == :warm && window[:attempts] >= @limit && elapsed.positive? && window[:bytes] / elapsed >= Policy::FAST_LINK_BYTES_PER_SECOND
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# The answers a window needs: max(window attempts, limit), except while warming: the limit for the warming window
|
|
240
|
+
# and the windows before a probe's cohort settles, and what the sampling rule asks for, at least the limit, for a
|
|
241
|
+
# baseline's or a probe's window. A rest's windows are of the ordinary size.
|
|
242
|
+
def window_target
|
|
243
|
+
ordinary = [ @policy.window_attempts, @limit ].max
|
|
244
|
+
return ordinary unless @phase == :warm
|
|
245
|
+
|
|
246
|
+
case @stage
|
|
247
|
+
when :baseline, :probing then [ @limit, @warm_target ].max
|
|
248
|
+
when :resting then ordinary
|
|
249
|
+
else @limit
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def close_window(now)
|
|
254
|
+
window = @window
|
|
255
|
+
elapsed = now - window[:started_at]
|
|
256
|
+
reset_window(now)
|
|
257
|
+
return unless elapsed.positive?
|
|
258
|
+
|
|
259
|
+
stats = {
|
|
260
|
+
inflight: window[:busy] / elapsed,
|
|
261
|
+
throughput: window[:bytes] / elapsed,
|
|
262
|
+
latency: window[:bytes].positive? ? window[:durations] / window[:bytes] : nil,
|
|
263
|
+
samples: window[:samples]
|
|
264
|
+
}
|
|
265
|
+
latency = stats[:latency]
|
|
266
|
+
@latency_min = latency if !latency.nil? && latency.positive? && (@latency_min.nil? || latency < @latency_min)
|
|
267
|
+
if stats[:inflight] < Policy::ELIGIBLE_OCCUPANCY * @limit
|
|
268
|
+
@ineligible_windows += 1
|
|
269
|
+
record(now, :ineligible, @limit)
|
|
270
|
+
elsif @phase == :warm
|
|
271
|
+
@judged_windows += 1
|
|
272
|
+
judge_warm(now, stats)
|
|
273
|
+
else
|
|
274
|
+
@judged_windows += 1
|
|
275
|
+
judge(now, stats)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# A judged warm window. The fast-link check comes first, whatever the window was for.
|
|
280
|
+
def judge_warm(now, stats)
|
|
281
|
+
note_warm(stats[:samples]) unless @stage == :settling
|
|
282
|
+
return promote(now) if stats[:throughput] >= Policy::FAST_LINK_BYTES_PER_SECOND
|
|
283
|
+
|
|
284
|
+
case @stage
|
|
285
|
+
when :warming
|
|
286
|
+
@stage = :baseline
|
|
287
|
+
@warm_target = required_samples(@limit, [ next_added, 1 ].max)
|
|
288
|
+
record(now, :warm_connection, @limit)
|
|
289
|
+
when :settling then record(now, :warm_settling, @limit)
|
|
290
|
+
when :baseline then rest_or_probe(now, stats)
|
|
291
|
+
when :probing then judge_probe(now, stats)
|
|
292
|
+
else
|
|
293
|
+
@rest_windows -= 1
|
|
294
|
+
@rest_windows.positive? ? record(now, :rest, @limit) : rest_or_probe(now, stats)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def promote(now)
|
|
299
|
+
@warm_probe = nil
|
|
300
|
+
@cohort = nil
|
|
301
|
+
@phase = :ordinary
|
|
302
|
+
change(now, [ @policy.start, @limit ].max, :fast_link)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# From a judged baseline, or the last window of a rest: rests again when that window's own warm queue estimate is
|
|
306
|
+
# already at least half what the next probe would add, and otherwise raises the target by that much.
|
|
307
|
+
def rest_or_probe(now, stats)
|
|
308
|
+
added = next_added
|
|
309
|
+
return finish_warm(now, :warm_complete) unless added.positive?
|
|
310
|
+
return rest(now, :warm_queued) if warm_queue(@limit, stats) >= 0.5 * added
|
|
311
|
+
|
|
312
|
+
@warm_probe = { from: @limit, added: added, reference: stats }
|
|
313
|
+
@warm_target = required_samples(@limit, added)
|
|
314
|
+
@stage = :settling
|
|
315
|
+
@cohort = nil
|
|
316
|
+
@answers = 0
|
|
317
|
+
change(now, @limit + added, :warm_probe)
|
|
318
|
+
note_cohort
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# The window judging a probe: the raise is kept, and the next probe follows, when its throughput beats the
|
|
322
|
+
# baseline's by half the share of attempts added and its warm queue estimate stays under d / 2; otherwise the
|
|
323
|
+
# target goes back to where the probe came from, to rest.
|
|
324
|
+
def judge_probe(now, stats)
|
|
325
|
+
probe = @warm_probe
|
|
326
|
+
return rest_or_probe(now, stats) unless probe
|
|
327
|
+
|
|
328
|
+
@warm_probe = nil
|
|
329
|
+
required = probe[:reference][:throughput] * (1 + (0.5 * probe[:added] / probe[:from]))
|
|
330
|
+
if stats[:throughput] >= required && warm_queue(probe[:from], stats, probe[:reference]) < 0.5 * probe[:added]
|
|
331
|
+
@rested = false
|
|
332
|
+
return finish_warm(now, :warm_complete) if @limit >= @policy.start
|
|
333
|
+
|
|
334
|
+
rest_or_probe(now, stats)
|
|
335
|
+
else
|
|
336
|
+
change(now, probe[:from], :warm_reject)
|
|
337
|
+
rest(now)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Rests HOLD_WINDOWS + GROWTH_INTERVAL_WINDOWS judged windows of ordinary size; the probe after it adds one.
|
|
342
|
+
def rest(now, reason = nil)
|
|
343
|
+
@stage = :resting
|
|
344
|
+
@rest_windows = Policy::HOLD_WINDOWS + Policy::GROWTH_INTERVAL_WINDOWS
|
|
345
|
+
@rested = true
|
|
346
|
+
@warm_probe = nil
|
|
347
|
+
@cohort = nil
|
|
348
|
+
record(now, reason, @limit) if reason
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# What the next warm probe adds: half the target, rounded up, or one after a rest, never past the start.
|
|
352
|
+
def next_added
|
|
353
|
+
[ @rested ? 1 : (@limit / 2.0).ceil, @policy.start - @limit ].min
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def finish_warm(now, reason)
|
|
357
|
+
@phase = :ordinary
|
|
358
|
+
record(now, reason, @limit)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# The attempts in flight when a probe's raised target first fills are its cohort, by their sequence numbers.
|
|
362
|
+
def note_cohort
|
|
363
|
+
return unless @phase == :warm && @stage == :settling && @cohort.nil? && @admitted >= @limit
|
|
364
|
+
|
|
365
|
+
@cohort = @outstanding.keys.to_set
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Once every attempt of a probe's cohort has ended, however it ended, the window opened then judges the probe.
|
|
369
|
+
def settle_cohort(attempt, now)
|
|
370
|
+
return unless @cohort&.delete?(attempt.sequence) && @cohort&.empty?
|
|
371
|
+
|
|
372
|
+
@cohort = nil
|
|
373
|
+
return unless @phase == :warm && @stage == :settling
|
|
374
|
+
|
|
375
|
+
@stage = :probing
|
|
376
|
+
reset_window(now)
|
|
377
|
+
record(now, :warm_settled, @limit)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# An accepted answer since a probe's raise: once max(window attempts, target) of them have arrived, the window
|
|
381
|
+
# then closing judges the probe, whether its cohort has settled or not.
|
|
382
|
+
def answered_since_raise
|
|
383
|
+
return unless @phase == :warm && @stage == :settling
|
|
384
|
+
|
|
385
|
+
@answers += 1
|
|
386
|
+
return if @answers < [ @policy.window_attempts, @limit ].max
|
|
387
|
+
|
|
388
|
+
@stage = :probing
|
|
389
|
+
@cohort = nil
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# A judged warm window's samples, each an accepted attempt's duration over its bytes: the fastest median at its
|
|
393
|
+
# target, and their squared deviations from their mean, relative to its square, with their degrees of freedom.
|
|
394
|
+
def note_warm(samples)
|
|
395
|
+
median = median(samples)
|
|
396
|
+
best = @warm_medians[@limit]
|
|
397
|
+
@warm_medians[@limit] = median if !median.nil? && median.positive? && (best.nil? || median < best)
|
|
398
|
+
return if samples.size < 2
|
|
399
|
+
|
|
400
|
+
mean = samples.sum / samples.size
|
|
401
|
+
return unless mean.positive?
|
|
402
|
+
|
|
403
|
+
@warm_spread += samples.sum { |sample| ((sample / mean) - 1)**2 }
|
|
404
|
+
@warm_degrees += samples.size - 1
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# I * (1 - reference median / current median duration per byte): how much of a window's concurrency queued,
|
|
408
|
+
# against the fastest median judged at target, or reference's when none was.
|
|
409
|
+
def warm_queue(target, stats, reference = nil)
|
|
410
|
+
before = @warm_medians[target] || (reference && median(reference[:samples]))
|
|
411
|
+
after = median(stats[:samples])
|
|
412
|
+
return 0.0 unless before&.positive? && after&.positive?
|
|
413
|
+
|
|
414
|
+
stats[:inflight] * (1 - (before / after))
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def median(samples)
|
|
418
|
+
sorted = samples.sort
|
|
419
|
+
return if sorted.empty?
|
|
420
|
+
|
|
421
|
+
middle = sorted.size / 2
|
|
422
|
+
sorted.size.odd? ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2.0
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# How many attempts the probe window needs to tell a gain of 0.5 * d / n from noise. With S the relative squared
|
|
426
|
+
# deviations note_warm summed over every judged warm window so far, this one included, and f their degrees of
|
|
427
|
+
# freedom, a = 2 / (9f) and h = 1 - a - 2 * sqrt(a), v = S / (f * h^3) bounds their variance from above (no
|
|
428
|
+
# bound when h is not positive), and the probe at its raised target r = n + d needs
|
|
429
|
+
# max(2, ceil(2 * v * (r * 2 / (0.5d))^2)), at most the window attempts; two without any spread or freedom.
|
|
430
|
+
def required_samples(from, added)
|
|
431
|
+
return 2 if @warm_degrees < 1 || !@warm_spread.positive? || !added.positive?
|
|
432
|
+
|
|
433
|
+
a = 2.0 / (9 * @warm_degrees)
|
|
434
|
+
h = 1 - a - (2 * Math.sqrt(a))
|
|
435
|
+
return @policy.window_attempts unless h.positive?
|
|
436
|
+
|
|
437
|
+
variance = @warm_spread / (@warm_degrees * (h**3))
|
|
438
|
+
raised = from + added
|
|
439
|
+
(2 * variance * (((raised * 2.0) / (0.5 * added))**2)).ceil.clamp(2, @policy.window_attempts)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def judge(now, stats)
|
|
443
|
+
queue = queue_estimate(stats)
|
|
444
|
+
probe = @probe
|
|
445
|
+
if queue >= @policy.queue_backoff
|
|
446
|
+
@probe = nil
|
|
447
|
+
shrink(now, Policy::LATENCY_SHRINK, @policy.latency_floor, :queue_backoff)
|
|
448
|
+
elsif probe
|
|
449
|
+
if queue < @policy.queue_suppress && stats[:throughput] >= probe[:baseline] * (1 + Policy::PROBE_GAIN)
|
|
450
|
+
@probe = nil
|
|
451
|
+
grow(now, stats, :probe_earned)
|
|
452
|
+
elsif queue < @policy.queue_suppress && (probe[:misses] += 1) < Policy::PROBE_WINDOWS
|
|
453
|
+
record(now, :probe_miss, @limit)
|
|
454
|
+
else
|
|
455
|
+
@probe = nil
|
|
456
|
+
@hold_windows = Policy::HOLD_WINDOWS
|
|
457
|
+
change(now, [ probe[:from], [ @policy.rejected_floor, @limit ].min ].max, :probe_rejected)
|
|
458
|
+
end
|
|
459
|
+
elsif queue >= @policy.queue_suppress
|
|
460
|
+
record(now, :queue_suppress, @limit)
|
|
461
|
+
elsif @hold_windows.positive?
|
|
462
|
+
@hold_windows -= 1
|
|
463
|
+
record(now, :hold, @limit)
|
|
464
|
+
else
|
|
465
|
+
grow(now, stats, :probe)
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def queue_estimate(stats)
|
|
470
|
+
latency = stats[:latency]
|
|
471
|
+
return 0.0 unless latency&.positive? && @latency_min
|
|
472
|
+
|
|
473
|
+
stats[:inflight] * (1 - (@latency_min / latency))
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def grow(now, stats, reason)
|
|
477
|
+
return record(now, reason == :probe ? :at_max : reason, @limit) if @limit >= @policy.max
|
|
478
|
+
|
|
479
|
+
step = @limit < @policy.soft ? @policy.growth_step : 1
|
|
480
|
+
@probe = { from: @limit, baseline: stats[:throughput], misses: 0 }
|
|
481
|
+
change(now, [ @limit + step, @policy.max ].min, reason)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Shrinks the limit by share, to no less than floor (or the limit, when that is lower) and by at least one.
|
|
485
|
+
# Returns whether it changed.
|
|
486
|
+
def shrink(now, share, floor, reason)
|
|
487
|
+
target = [ (@limit * (1 - share)).floor, @limit - 1 ].min
|
|
488
|
+
target = [ target, [ floor, @limit ].min, 1 ].max
|
|
489
|
+
return false if target >= @limit
|
|
490
|
+
|
|
491
|
+
change(now, target, reason)
|
|
492
|
+
true
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# A failure shrinks the limit only for an attempt from the current epoch; while warming, down to one, and in any
|
|
496
|
+
# epoch it abandons the probe and starts the rest again.
|
|
497
|
+
def failed(attempt, now)
|
|
498
|
+
return if @phase == :fixed
|
|
499
|
+
return warm_interrupted(now, attempt.epoch == @epoch && shrink(now, Policy::FAILURE_SHRINK, 1, :failure)) if @phase == :warm
|
|
500
|
+
return unless attempt.epoch == @epoch
|
|
501
|
+
|
|
502
|
+
@probe = nil
|
|
503
|
+
shrink(now, Policy::FAILURE_SHRINK, @policy.min, :failure)
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# A throttle shrinks the limit only for an attempt from the current epoch (while warming, down to one), which starts
|
|
507
|
+
# a new epoch. Every throttle, in any epoch, starts or extends a cooldown and restarts measurement; one that changed
|
|
508
|
+
# no limit keeps the epoch, so attempts sent since the last change still count their failures. While warming, in
|
|
509
|
+
# any epoch, it abandons the probe and rests again. A fixed policy learns nothing from it.
|
|
510
|
+
def throttled(attempt, now, retry_after)
|
|
511
|
+
return if @phase == :fixed
|
|
512
|
+
|
|
513
|
+
cooldown = now + [ Policy::THROTTLE_COOLDOWN, retry_after.to_f ].max
|
|
514
|
+
@cooldown_until = [ @cooldown_until || cooldown, cooldown ].max
|
|
515
|
+
current = attempt.epoch == @epoch
|
|
516
|
+
@probe = nil if current
|
|
517
|
+
shrunk = current && shrink(now, Policy::THROTTLE_SHRINK, @phase == :warm ? 1 : @policy.min, :throttle)
|
|
518
|
+
unless shrunk
|
|
519
|
+
reset_window(now)
|
|
520
|
+
record(now, :throttle_cooldown, @limit)
|
|
521
|
+
end
|
|
522
|
+
rest(now) if @phase == :warm
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# A failure while warming, in any epoch: the probe is abandoned without being judged, and the rest starts again,
|
|
526
|
+
# at whatever target the failure left.
|
|
527
|
+
def warm_interrupted(now, shrunk)
|
|
528
|
+
reset_window(now) unless shrunk
|
|
529
|
+
rest(now, shrunk ? nil : :warm_interrupted)
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def change(now, to, reason)
|
|
533
|
+
from = @limit
|
|
534
|
+
if to != from
|
|
535
|
+
@limit = to
|
|
536
|
+
@epoch += 1
|
|
537
|
+
reset_window(now)
|
|
538
|
+
end
|
|
539
|
+
record(now, reason, from)
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def record(now, reason, from)
|
|
543
|
+
@decisions.shift if @decisions.size >= MAX_DECISIONS
|
|
544
|
+
@decisions << { at: now, phase: @phase, from: from, to: @limit, reason: reason }.freeze
|
|
545
|
+
nil
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
end
|