blob: e4f9d413986c6de920141af18d29dbd3e1bc2b0c [file] [log] [blame]
using J2N.Threading;
using System;
#if FEATURE_SERIALIZABLE_EXCEPTIONS
using System.Runtime.Serialization;
using System.Security.Permissions;
#endif
using System.Threading;
namespace Lucene.Net.Search
{
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using AtomicReaderContext = Lucene.Net.Index.AtomicReaderContext;
using Counter = Lucene.Net.Util.Counter;
/// <summary>
/// The <see cref="TimeLimitingCollector"/> is used to timeout search requests that
/// take longer than the maximum allowed search time limit. After this time is
/// exceeded, the search thread is stopped by throwing a
/// <see cref="TimeExceededException"/>.
/// </summary>
public class TimeLimitingCollector : ICollector
{
/// <summary>
/// Thrown when elapsed search time exceeds allowed search time. </summary>
// LUCENENET: It is no longer good practice to use binary serialization.
// See: https://github.com/dotnet/corefx/issues/23584#issuecomment-325724568
#if FEATURE_SERIALIZABLE_EXCEPTIONS
[Serializable]
#endif
public class TimeExceededException : Exception
{
private long timeAllowed;
private long timeElapsed;
private int lastDocCollected;
internal TimeExceededException(long timeAllowed, long timeElapsed, int lastDocCollected)
: base("Elapsed time: " + timeElapsed + "Exceeded allowed search time: " + timeAllowed + " ms.")
{
this.timeAllowed = timeAllowed;
this.timeElapsed = timeElapsed;
this.lastDocCollected = lastDocCollected;
}
// For testing purposes
internal TimeExceededException(string message)
: base(message)
{
}
#if FEATURE_SERIALIZABLE_EXCEPTIONS
/// <summary>
/// Initializes a new instance of this class with serialized data.
/// </summary>
/// <param name="info">The <see cref="SerializationInfo"/> that holds the serialized object data about the exception being thrown.</param>
/// <param name="context">The <see cref="StreamingContext"/> that contains contextual information about the source or destination.</param>
protected TimeExceededException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
timeAllowed = info.GetInt64("timeAllowed");
timeElapsed = info.GetInt64("timeElapsed");
lastDocCollected = info.GetInt32("lastDocCollected");
}
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue("timeAllowed", timeAllowed, typeof(long));
info.AddValue("timeElapsed", timeElapsed, typeof(long));
info.AddValue("lastDocCollected", lastDocCollected, typeof(int));
}
#endif
/// <summary>
/// Returns allowed time (milliseconds). </summary>
public virtual long TimeAllowed => timeAllowed;
/// <summary>
/// Returns elapsed time (milliseconds). </summary>
public virtual long TimeElapsed => timeElapsed;
/// <summary>
/// Returns last doc (absolute doc id) that was collected when the search time exceeded. </summary>
public virtual int LastDocCollected => lastDocCollected;
}
private long t0 = long.MinValue;
private long timeout = long.MinValue;
private ICollector collector;
private readonly Counter clock;
private readonly long ticksAllowed;
private bool greedy = false;
private int docBase;
/// <summary>
/// Create a <see cref="TimeLimitingCollector"/> wrapper over another <see cref="ICollector"/> with a specified timeout. </summary>
/// <param name="collector"> The wrapped <see cref="ICollector"/> </param>
/// <param name="clock"> The timer clock </param>
/// <param name="ticksAllowed"> Max time allowed for collecting
/// hits after which <see cref="TimeExceededException"/> is thrown </param>
public TimeLimitingCollector(ICollector collector, Counter clock, long ticksAllowed)
{
this.collector = collector;
this.clock = clock;
this.ticksAllowed = ticksAllowed;
}
/// <summary>
/// Sets the baseline for this collector. By default the collectors baseline is
/// initialized once the first reader is passed to the collector.
/// To include operations executed in prior to the actual document collection
/// set the baseline through this method in your prelude.
/// <para>
/// Example usage:
/// <code>
/// // Counter is in the Lucene.Net.Util namespace
/// Counter clock = Counter.NewCounter(true);
/// long baseline = clock.Get();
/// // ... prepare search
/// TimeLimitingCollector collector = new TimeLimitingCollector(c, clock, numTicks);
/// collector.SetBaseline(baseline);
/// indexSearcher.Search(query, collector);
/// </code>
/// </para>
/// </summary>
/// <seealso cref="SetBaseline()"/>
public virtual void SetBaseline(long clockTime)
{
t0 = clockTime;
timeout = t0 + ticksAllowed;
}
/// <summary>
/// Syntactic sugar for <see cref="SetBaseline(long)"/> using <see cref="Counter.Get()"/>
/// on the clock passed to the constructor.
/// </summary>
public virtual void SetBaseline()
{
SetBaseline(clock.Get());
}
/// <summary>
/// Checks if this time limited collector is greedy in collecting the last hit.
/// A non greedy collector, upon a timeout, would throw a <see cref="TimeExceededException"/>
/// without allowing the wrapped collector to collect current doc. A greedy one would
/// first allow the wrapped hit collector to collect current doc and only then
/// throw a <see cref="TimeExceededException"/>.
/// </summary>
public virtual bool IsGreedy
{
get => greedy;
set => this.greedy = value;
}
/// <summary>
/// Calls <see cref="ICollector.Collect(int)"/> on the decorated <see cref="ICollector"/>
/// unless the allowed time has passed, in which case it throws an exception.
/// </summary>
/// <exception cref="TimeExceededException">
/// If the time allowed has exceeded. </exception>
public virtual void Collect(int doc)
{
long time = clock.Get();
if (timeout < time)
{
if (greedy)
{
//System.out.println(this+" greedy: before failing, collecting doc: "+(docBase + doc)+" "+(time-t0));
collector.Collect(doc);
}
//System.out.println(this+" failing on: "+(docBase + doc)+" "+(time-t0));
throw new TimeExceededException(timeout - t0, time - t0, docBase + doc);
}
//System.out.println(this+" collecting: "+(docBase + doc)+" "+(time-t0));
collector.Collect(doc);
}
public virtual void SetNextReader(AtomicReaderContext context)
{
collector.SetNextReader(context);
this.docBase = context.DocBase;
if (long.MinValue == t0)
{
SetBaseline();
}
}
public virtual void SetScorer(Scorer scorer)
{
collector.SetScorer(scorer);
}
public virtual bool AcceptsDocsOutOfOrder => collector.AcceptsDocsOutOfOrder;
/// <summary>
/// This is so the same timer can be used with a multi-phase search process such as grouping.
/// We don't want to create a new <see cref="TimeLimitingCollector"/> for each phase because that would
/// reset the timer for each phase. Once time is up subsequent phases need to timeout quickly.
/// </summary>
/// <param name="collector"> The actual collector performing search functionality. </param>
public virtual void SetCollector(ICollector collector)
{
this.collector = collector;
}
/// <summary>
/// Returns the global <see cref="TimerThread"/>'s <see cref="Counter"/>
/// <para>
/// Invoking this creates may create a new instance of <see cref="TimerThread"/> iff
/// the global <see cref="TimerThread"/> has never been accessed before. The thread
/// returned from this method is started on creation and will be alive unless
/// you stop the <see cref="TimerThread"/> via <see cref="TimerThread.StopTimer()"/>.
/// </para>
/// @lucene.experimental
/// </summary>
/// <returns> the global TimerThreads <seealso cref="Counter"/> </returns>
public static Counter GlobalCounter => TimerThreadHolder.THREAD.counter;
/// <summary>
/// Returns the global <see cref="TimerThread"/>.
/// <para>
/// Invoking this creates may create a new instance of <see cref="TimerThread"/> iff
/// the global <see cref="TimerThread"/> has never been accessed before. The thread
/// returned from this method is started on creation and will be alive unless
/// you stop the <see cref="TimerThread"/> via <see cref="TimerThread.StopTimer()"/>.
/// </para>
/// @lucene.experimental
/// </summary>
/// <returns> the global <see cref="TimerThread"/> </returns>
public static TimerThread GlobalTimerThread => TimerThreadHolder.THREAD;
private sealed class TimerThreadHolder
{
internal static readonly TimerThread THREAD = LoadTimerThread(); // LUCENENET: Avoid static constructors (see https://github.com/apache/lucenenet/pull/224#issuecomment-469284006)
private static TimerThread LoadTimerThread()
{
var thread = new TimerThread(Counter.NewCounter(true));
thread.Start();
return thread;
}
}
/// <summary>
/// Thread used to timeout search requests.
/// Can be stopped completely with <see cref="TimerThread.StopTimer()"/>
/// <para/>
/// @lucene.experimental
/// </summary>
public sealed class TimerThread : ThreadJob
{
public const string THREAD_NAME = "TimeLimitedCollector timer thread";
public const int DEFAULT_RESOLUTION = 20;
// NOTE: we can avoid explicit synchronization here for several reasons:
// * updates to volatile long variables are atomic
// * only single thread modifies this value
// * use of volatile keyword ensures that it does not reside in
// a register, but in main memory (so that changes are visible to
// other threads).
// * visibility of changes does not need to be instantaneous, we can
// afford losing a tick or two.
//
// See section 17 of the Java Language Specification for details.
private long time = 0;
private volatile bool stop = false;
private long resolution;
internal readonly Counter counter;
public TimerThread(long resolution, Counter counter)
: base(THREAD_NAME)
{
this.resolution = resolution;
this.counter = counter;
this.IsBackground = (true);
}
public TimerThread(Counter counter)
: this(DEFAULT_RESOLUTION, counter)
{
}
public override void Run()
{
while (!stop)
{
// TODO: Use System.nanoTime() when Lucene moves to Java SE 5.
counter.AddAndGet(resolution);
//#if FEATURE_THREAD_INTERRUPT
// try
// {
//#endif
Thread.Sleep(TimeSpan.FromMilliseconds(Interlocked.Read(ref resolution)));
//#if FEATURE_THREAD_INTERRUPT // LUCENENET NOTE: Senseless to catch and rethrow the same exception type
// }
// catch (ThreadInterruptedException ie)
// {
// throw new ThreadInterruptedException("Thread Interrupted Exception", ie);
// }
//#endif
}
}
/// <summary>
/// Get the timer value in milliseconds.
/// </summary>
public long Milliseconds => time;
/// <summary>
/// Stops the timer thread
/// </summary>
public void StopTimer()
{
stop = true;
}
/// <summary>
/// Return the timer resolution. </summary>
public long Resolution
{
get => resolution;
set => this.resolution = Math.Max(value, 5); // 5 milliseconds is about the minimum reasonable time for a Object.wait(long) call.
}
}
}
}