public IState ChangeState(StateChangeContext context)
        {
            // To ensure that job state will be changed only from one of the
            // specified states, we need to ensure that other users/workers
            // are not able to change the state of the job during the
            // execution of this method. To guarantee this behavior, we are
            // using distributed application locks and rely on fact, that
            // any state transitions will be made only within a such lock.
            using (context.Connection.AcquireDistributedJobLock(context.BackgroundJobId, JobLockTimeout))
            {
                var jobData = GetJobData(context);

                if (jobData == null)
                {
                    // The job does not exist. This may happen, because not
                    // all storage backends support foreign keys.
                    return null;
                }

                if (context.ExpectedStates != null && !context.ExpectedStates.Contains(jobData.State, StringComparer.OrdinalIgnoreCase))
                {
                    return null;
                }
                
                var appliedState = context.NewState;

                try
                {
                    jobData.EnsureLoaded();
                }
                catch (JobLoadException ex)
                {
                    // If the job type could not be loaded, we are unable to
                    // load corresponding filters, unable to process the job
                    // and sometimes unable to change its state (the enqueued
                    // state depends on the type of a job).

                    if (!appliedState.IgnoreJobLoadException)
                    {
                        appliedState = new FailedState(ex.InnerException)
                        {
                            Reason = String.Format(
                                "Can not change the state to '{0}': target method was not found.",
                                appliedState.Name)
                        };
                    }
                }

                var backgroundJob = new BackgroundJob(context.BackgroundJobId, jobData.Job, jobData.CreatedAt);
                appliedState = ChangeState(context, backgroundJob, appliedState, jobData.State);

                return appliedState;
            }
        }
예제 #2
0
        public IState ChangeState(StateChangeContext context)
        {
            // To ensure that job state will be changed only from one of the
            // specified states, we need to ensure that other users/workers
            // are not able to change the state of the job during the
            // execution of this method. To guarantee this behavior, we are
            // using distributed application locks and rely on fact, that
            // any state transitions will be made only within a such lock.
            using (context.Connection.AcquireDistributedJobLock(context.BackgroundJobId, JobLockTimeout))
            {
                var jobData = GetJobData(context);

                if (jobData == null)
                {
                    // The job does not exist. This may happen, because not
                    // all storage backends support foreign keys.
                    return(null);
                }

                if (context.ExpectedStates != null && !context.ExpectedStates.Contains(jobData.State, StringComparer.OrdinalIgnoreCase))
                {
                    return(null);
                }

                var appliedState = context.NewState;

                try
                {
                    jobData.EnsureLoaded();
                }
                catch (JobLoadException ex)
                {
                    // If the job type could not be loaded, we are unable to
                    // load corresponding filters, unable to process the job
                    // and sometimes unable to change its state (the enqueued
                    // state depends on the type of a job).

                    if (!appliedState.IgnoreJobLoadException)
                    {
                        appliedState = new FailedState(ex.InnerException)
                        {
                            Reason = String.Format(
                                "Can not change the state to '{0}': target method was not found.",
                                appliedState.Name)
                        };
                    }
                }

                var backgroundJob = new BackgroundJob(context.BackgroundJobId, jobData.Job, jobData.CreatedAt);
                appliedState = ChangeState(context, backgroundJob, appliedState, jobData.State);

                return(appliedState);
            }
        }
예제 #3
0
        private IState ChangeState(
            StateChangeContext context, BackgroundJob backgroundJob, IState toState, string oldStateName)
        {
            Exception exception = null;

            for (var i = 0; i < MaxStateChangeAttempts; i++)
            {
                try
                {
                    using (var transaction = context.Connection.CreateWriteTransaction())
                    {
                        var applyContext = new ApplyStateContext(
                            context.Storage,
                            context.Connection,
                            transaction,
                            backgroundJob,
                            toState,
                            oldStateName);

                        var appliedState = _stateMachine.ApplyState(applyContext);

                        transaction.Commit();

                        return(appliedState);
                    }
                }
                catch (Exception ex)
                {
                    exception = ex;
                }
            }

            var failedState = new FailedState(exception)
            {
                Reason = $"Failed to change state to a '{toState.Name}' one due to an exception after {MaxStateChangeAttempts} retry attempts"
            };

            using (var transaction = context.Connection.CreateWriteTransaction())
            {
                _coreStateMachine.ApplyState(new ApplyStateContext(
                                                 context.Storage,
                                                 context.Connection,
                                                 transaction,
                                                 backgroundJob,
                                                 failedState,
                                                 oldStateName));

                transaction.Commit();
            }

            return(failedState);
        }
예제 #4
0
        private IState ChangeState(
            StateChangeContext context, BackgroundJob backgroundJob, IState toState, string oldStateName)
        {
            using (var transaction = context.Connection.CreateWriteTransaction())
            {
                var applyContext = new ApplyStateContext(
                    context.Storage,
                    context.Connection,
                    transaction,
                    backgroundJob,
                    toState,
                    oldStateName);

                var appliedState = _stateMachine.ApplyState(applyContext);

                transaction.Commit();

                return(appliedState);
            }
        }
예제 #5
0
        private static JobData GetJobData(StateChangeContext context)
        {
            var firstAttempt = true;

            while (true)
            {
                var jobData = context.Connection.GetJobData(context.BackgroundJobId);
                if (jobData != null && !String.IsNullOrEmpty(jobData.State))
                {
                    return(jobData);
                }

                if (context.CancellationToken.IsCancellationRequested)
                {
                    return(null);
                }

                Thread.Sleep(firstAttempt ? 0 : 100);
                firstAttempt = false;
            }
        }
예제 #6
0
        public IState ChangeState(StateChangeContext context)
        {
            // To ensure that job state will be changed only from one of the
            // specified states, we need to ensure that other users/workers
            // are not able to change the state of the job during the
            // execution of this method. To guarantee this behavior, we are
            // using distributed application locks and rely on fact, that
            // any state transitions will be made only within a such lock.
            using (context.Connection.AcquireDistributedJobLock(context.BackgroundJobId, JobLockTimeout))
            {
                var jobData = GetJobData(context);

                if (jobData == null)
                {
                    return(null);
                }

                if (context.ExpectedStates != null && !context.ExpectedStates.Contains(jobData.State, StringComparer.OrdinalIgnoreCase))
                {
                    return(null);
                }

                var stateToApply = context.NewState;

                try
                {
                    jobData.EnsureLoaded();
                }
                catch (JobLoadException ex)
                {
                    // This happens when Hangfire couldn't find the target method,
                    // serialized within a background job. There are many reasons
                    // for this case, including refactored code, or a missing
                    // assembly reference due to a mistake or erroneous deployment.
                    //
                    // The problem is that in this case we can't get any filters,
                    // applied at a method or a class level, and we can't proceed
                    // with the state change without breaking a consistent behavior:
                    // in some cases our filters will be applied, and in other ones
                    // will not.

                    // TODO 1.X/2.0:
                    // There's a problem with filters related to handling the states
                    // which ignore this exception, i.e. fitlers for the FailedState
                    // and the DeletedState, such as AutomaticRetryAttrubute filter.
                    //
                    // We should document that such a filters may not be fired, when
                    // we can't find a target method, and these filters should be
                    // applied only at the global level to get consistent results.
                    //
                    // In 2.0 we should have a special state for all the errors, when
                    // Hangfire doesn't know what to do, without any possibility to
                    // add method or class-level filters for such a state to provide
                    // the same behavior no matter what.

                    if (!stateToApply.IgnoreJobLoadException)
                    {
                        stateToApply = new FailedState(ex.InnerException)
                        {
                            Reason = $"Can not change the state to '{stateToApply.Name}': target method was not found."
                        };
                    }
                }

                using (var transaction = context.Connection.CreateWriteTransaction())
                {
                    var applyContext = new ApplyStateContext(
                        context.Storage,
                        context.Connection,
                        transaction,
                        new BackgroundJob(context.BackgroundJobId, jobData.Job, jobData.CreatedAt),
                        stateToApply,
                        jobData.State);

                    var appliedState = _stateMachine.ApplyState(applyContext);

                    transaction.Commit();

                    return(appliedState);
                }
            }
        }
예제 #7
0
        private static JobData GetJobData(StateChangeContext context)
        {
            // This code was introduced as a fix for an issue, which appeared when an
            // external queue implementation was used together with a non-linearizable
            // storage. The problem was likely related to the SQL Azure + Azure ServiceBus
            // (or RabbitMQ or so) bundle, because the READCOMMITTED_SNAPSHOT_ON setting
            // is enabled by default there.
            //
            // Since external queueing doesn't share the linearization point with the
            // storage, it is possible that a worker will pick up a background job before
            // its transaction was committed. Non-linearizable read will simply return
            // the NULL value instead of waiting for a transaction to be committed. With
            // this code, we will make several retry attempts to handle this case to wait
            // on the client side.
            //
            // On the other hand, we need to give up after some retry attempt, because
            // we should also handle the case, when our queue and job storage became
            // unsynchronized with each other due to failures, manual intervention or so.
            // Otherwise we will wait forever in this cases, since And there's no way to
            // make a distinction between a non-linearizable read and the storages, non-
            // synchronized with each other.
            //
            // In recent versions, Hangfire.SqlServer uses query hints to make all the
            // reads linearizable no matter what, but there may be other storages that
            // still require this workaround.

            // TODO 2.0:
            // Eliminate the need of this timeout by placing an explicit requirement to
            // storage implementations to either have a single linearization point for all
            // the operations inside a transaction; or make all the reads linearizable and
            // execute queueing operations after all the other ones in a transaction.

            var firstAttempt = true;

            while (true)
            {
                var jobData = context.Connection.GetJobData(context.BackgroundJobId);

                // Empty state means our job wasn't moved to any state after its creation.
                // Such a jobs may be created by internal logic, and those jobs have very
                // special meaning, thus we shouldn't allow state changer to alter them,
                // using this class (which can be used by users), leaving this logic to
                // low level API only, i.e. state machine.

                // TODO 1.X:
                // However, we shouldn't wait for the initial state change, because in some
                // cases (like in batches) it may take days. We should throw an exception
                // instead, clearly indicating that such a state change is prohibited. There
                // may be some issues on GitHub, related to the hanging dashboard requests
                // in this case.

                if (!String.IsNullOrEmpty(jobData?.State))
                {
                    return(jobData);
                }

                // State change can also be requested from user's request processing logic.
                // There is always a chance it will be issued against a non-existing or an
                // already expired background job, and a minute wait (or whatever timeout is
                // used) is completely unnecessary in this case.
                //
                // Since waiting is only required when a worker picks up a job, and
                // cancellation tokens are used only by the Worker class, we can avoid the
                // unnecessary waiting logic when no cancellation token is passed.

                if (context.CancellationToken.IsCancellationRequested ||
                    context.CancellationToken == CancellationToken.None)
                {
                    return(null);
                }

                context.CancellationToken.WaitHandle.WaitOne(firstAttempt ? 0 : 100);
                firstAttempt = false;
            }
        }
        private static JobData GetJobData(StateChangeContext context)
        {
            var firstAttempt = true;

            while (true)
            {
                var jobData = context.Connection.GetJobData(context.BackgroundJobId);
                if (jobData != null && !String.IsNullOrEmpty(jobData.State))
                {
                    return jobData;
                }

                if (context.CancellationToken.IsCancellationRequested)
                {
                    return null;
                }

                Thread.Sleep(firstAttempt ? 0 : 100);
                firstAttempt = false;
            }
        }
        private IState ChangeState(
            StateChangeContext context, BackgroundJob backgroundJob, IState toState, string oldStateName)
        {
            using (var transaction = context.Connection.CreateWriteTransaction())
            {
                var applyContext = new ApplyStateContext(
                    context.Storage,
                    context.Connection,
                    transaction,
                    backgroundJob,
                    toState,
                    oldStateName);

                var appliedState = _stateMachine.ApplyState(applyContext);

                transaction.Commit();

                return appliedState;
            }
        }
예제 #10
0
        public IState ChangeState(StateChangeContext context)
        {
            // To ensure that job state will be changed only from one of the
            // specified states, we need to ensure that other users/workers
            // are not able to change the state of the job during the
            // execution of this method. To guarantee this behavior, we are
            // using distributed application locks and rely on fact, that
            // any state transitions will be made only within a such lock.
            using (context.Connection.AcquireDistributedJobLock(context.BackgroundJobId, JobLockTimeout))
            {
                var jobData = GetJobData(context);

                if (jobData == null)
                {
                    return(null);
                }

                if (context.ExpectedStates != null && !context.ExpectedStates.Contains(jobData.State, StringComparer.OrdinalIgnoreCase))
                {
                    return(null);
                }

                var stateToApply = context.NewState;

                try
                {
                    jobData.EnsureLoaded();
                }
                catch (JobLoadException ex)
                {
                    // This happens when Hangfire couldn't find the target method,
                    // serialized within a background job. There are many reasons
                    // for this case, including refactored code, or a missing
                    // assembly reference due to a mistake or erroneous deployment.
                    //
                    // The problem is that in this case we can't get any filters,
                    // applied at a method or a class level, and we can't proceed
                    // with the state change without breaking a consistent behavior:
                    // in some cases our filters will be applied, and in other ones
                    // will not.
                    if (!stateToApply.IgnoreJobLoadException)
                    {
                        stateToApply = new FailedState(ex.InnerException)
                        {
                            Reason = $"Can not change the state to '{stateToApply.Name}': target method was not found."
                        };
                    }
                }

                using (var transaction = context.Connection.CreateWriteTransaction())
                {
                    var applyContext = new ApplyStateContext(
                        context.Storage,
                        context.Connection,
                        transaction,
                        new BackgroundJob(context.BackgroundJobId, jobData.Job, jobData.CreatedAt),
                        stateToApply,
                        jobData.State,
                        context.Profiler);

                    // State changing process can fail due to an exception in state filters themselves,
                    // and DisableFilters property will cause state machine to perform a state transition
                    // without calling any filters. This is required when all the other state change
                    // attempts failed and we need to remove such a job from the processing pipeline.
                    // In this case all the filters are ignored, which may lead to confusion, so it's
                    // highly recommended to use the DisableFilters property only when changing state
                    // to the FailedState.
                    var stateMachine = context.DisableFilters ? _innerStateMachine : _stateMachine;
                    var appliedState = stateMachine.ApplyState(applyContext);

                    transaction.Commit();

                    return(appliedState);
                }
            }
        }