blob: 84aaa1727e2c0ac1a1e55f718731e5ee57a12412 [file] [log] [blame]
using Lucene.Net.Support;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
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 DirectoryReader = Lucene.Net.Index.DirectoryReader;
using IOUtils = Lucene.Net.Util.IOUtils;
/// <summary>
/// Keeps track of current plus old <see cref="IndexSearcher"/>s, disposing
/// the old ones once they have timed out.
///
/// Use it like this:
///
/// <code>
/// SearcherLifetimeManager mgr = new SearcherLifetimeManager();
/// </code>
///
/// Per search-request, if it's a "new" search request, then
/// obtain the latest searcher you have (for example, by
/// using <see cref="SearcherManager"/>), and then record this
/// searcher:
///
/// <code>
/// // Record the current searcher, and save the returend
/// // token into user's search results (eg as a hidden
/// // HTML form field):
/// long token = mgr.Record(searcher);
/// </code>
///
/// When a follow-up search arrives, for example the user
/// clicks next page, drills down/up, etc., take the token
/// that you saved from the previous search and:
///
/// <code>
/// // If possible, obtain the same searcher as the last
/// // search:
/// IndexSearcher searcher = mgr.Acquire(token);
/// if (searcher != null)
/// {
/// // Searcher is still here
/// try
/// {
/// // do searching...
/// }
/// finally
/// {
/// mgr.Release(searcher);
/// // Do not use searcher after this!
/// searcher = null;
/// }
/// }
/// else
/// {
/// // Searcher was pruned -- notify user session timed
/// // out, or, pull fresh searcher again
/// }
/// </code>
///
/// Finally, in a separate thread, ideally the same thread
/// that's periodically reopening your searchers, you should
/// periodically prune old searchers:
///
/// <code>
/// mgr.Prune(new PruneByAge(600.0));
/// </code>
///
/// <para><b>NOTE</b>: keeping many searchers around means
/// you'll use more resources (open files, RAM) than a single
/// searcher. However, as long as you are using
/// <see cref="DirectoryReader.OpenIfChanged(DirectoryReader)"/>, the searchers
/// will usually share almost all segments and the added resource usage
/// is contained. When a large merge has completed, and
/// you reopen, because that is a large change, the new
/// searcher will use higher additional RAM than other
/// searchers; but large merges don't complete very often and
/// it's unlikely you'll hit two of them in your expiration
/// window. Still you should budget plenty of heap in the
/// runtime to have a good safety margin.</para>
/// </summary>
public class SearcherLifetimeManager : IDisposable
{
internal const double NANOS_PER_SEC = 1000000000.0;
private sealed class SearcherTracker : IComparable<SearcherTracker>, IDisposable
{
public IndexSearcher Searcher { get; private set; }
public double RecordTimeSec { get; private set; }
public long Version { get; private set; }
public SearcherTracker(IndexSearcher searcher)
{
Searcher = searcher;
Version = ((DirectoryReader)searcher.IndexReader).Version;
searcher.IndexReader.IncRef();
// Use nanoTime not currentTimeMillis since it [in
// theory] reduces risk from clock shift
RecordTimeSec = Time.NanoTime() / NANOS_PER_SEC;
}
// Newer searchers are sort before older ones:
public int CompareTo(SearcherTracker other)
{
return other.RecordTimeSec.CompareTo(RecordTimeSec);
}
public void Dispose()
{
lock (this)
{
Searcher.IndexReader.DecRef();
}
}
}
private volatile bool _closed;
// TODO: we could get by w/ just a "set"; need to have
// Tracker hash by its version and have compareTo(Long)
// compare to its version
private readonly ConcurrentDictionary<long, Lazy<SearcherTracker>> _searchers = new ConcurrentDictionary<long, Lazy<SearcherTracker>>();
private void EnsureOpen()
{
if (_closed)
{
throw new ObjectDisposedException(this.GetType().FullName, "this SearcherLifetimeManager instance is closed");
}
}
/// <summary>
/// Records that you are now using this <see cref="IndexSearcher"/>.
/// Always call this when you've obtained a possibly new
/// <see cref="IndexSearcher"/>, for example from
/// <see cref="SearcherManager"/>. It's fine if you already passed the
/// same searcher to this method before.
///
/// <para>This returns the <see cref="long"/> token that you can later pass
/// to <see cref="Acquire(long)"/> to retrieve the same <see cref="IndexSearcher"/>.
/// You should record this <see cref="long"/> token in the search results
/// sent to your user, such that if the user performs a
/// follow-on action (clicks next page, drills down, etc.)
/// the token is returned.</para>
/// </summary>
public virtual long Record(IndexSearcher searcher)
{
EnsureOpen();
// TODO: we don't have to use IR.getVersion to track;
// could be risky (if it's buggy); we could get better
// bug isolation if we assign our own private ID:
var version = ((DirectoryReader)searcher.IndexReader).Version;
var factoryMethodCalled = false;
var tracker = _searchers.GetOrAdd(version, l => new Lazy<SearcherTracker>(() => { factoryMethodCalled = true; return new SearcherTracker(searcher); })).Value;
if (!factoryMethodCalled && tracker.Searcher != searcher)
{
throw new ArgumentException("the provided searcher has the same underlying reader version yet the searcher instance differs from before (new=" + searcher + " vs old=" + tracker.Searcher);
}
return version;
}
/// <summary>
/// Retrieve a previously recorded <see cref="IndexSearcher"/>, if it
/// has not yet been closed.
///
/// <para><b>NOTE</b>: this may return <c>null</c> when the
/// requested searcher has already timed out. When this
/// happens you should notify your user that their session
/// timed out and that they'll have to restart their
/// search.</para>
///
/// <para>If this returns a non-null result, you must match
/// later call <see cref="Release(IndexSearcher)"/> on this searcher, best
/// from a finally clause.</para>
/// </summary>
public virtual IndexSearcher Acquire(long version)
{
EnsureOpen();
Lazy<SearcherTracker> tracker;
if (_searchers.TryGetValue(version, out tracker) && tracker.IsValueCreated && tracker.Value.Searcher.IndexReader.TryIncRef())
{
return tracker.Value.Searcher;
}
return null;
}
/// <summary>
/// Release a searcher previously obtained from
/// <see cref="Acquire(long)"/>.
///
/// <para/><b>NOTE</b>: it's fine to call this after Dispose().
/// </summary>
public virtual void Release(IndexSearcher s)
{
s.IndexReader.DecRef();
}
/// <summary>
/// See <see cref="Prune(IPruner)"/>. </summary>
public interface IPruner
{
/// <summary>
/// Return <c>true</c> if this searcher should be removed. </summary>
/// <param name="ageSec"> How much time has passed since this
/// searcher was the current (live) searcher </param>
/// <param name="searcher"> Searcher </param>
bool DoPrune(double ageSec, IndexSearcher searcher);
}
/// <summary>
/// Simple pruner that drops any searcher older by
/// more than the specified seconds, than the newest
/// searcher.
/// </summary>
public sealed class PruneByAge : IPruner
{
private readonly double maxAgeSec;
public PruneByAge(double maxAgeSec)
{
if (maxAgeSec < 0)
{
throw new ArgumentException("maxAgeSec must be > 0 (got " + maxAgeSec + ")");
}
this.maxAgeSec = maxAgeSec;
}
public bool DoPrune(double ageSec, IndexSearcher searcher)
{
return ageSec > maxAgeSec;
}
}
/// <summary>
/// Calls provided <see cref="IPruner"/> to prune entries. The
/// entries are passed to the <see cref="IPruner"/> in sorted (newest to
/// oldest <see cref="IndexSearcher"/>) order.
///
/// <para/><b>NOTE</b>: you must peridiocally call this, ideally
/// from the same background thread that opens new
/// searchers.
/// </summary>
public virtual void Prune(IPruner pruner)
{
lock (this)
{
// Cannot just pass searchers.values() to ArrayList ctor
// (not thread-safe since the values can change while
// ArrayList is init'ing itself); must instead iterate
// ourselves:
var trackers = _searchers.Values.Select(item => item.Value).ToList();
trackers.Sort();
var lastRecordTimeSec = 0.0;
double now = Time.NanoTime() / NANOS_PER_SEC;
foreach (var tracker in trackers)
{
double ageSec;
if (lastRecordTimeSec == 0.0)
{
ageSec = 0.0;
}
else
{
ageSec = now - lastRecordTimeSec;
}
// First tracker is always age 0.0 sec, since it's
// still "live"; second tracker's age (= seconds since
// it was "live") is now minus first tracker's
// recordTime, etc:
if (pruner.DoPrune(ageSec, tracker.Searcher))
{
//System.out.println("PRUNE version=" + tracker.version + " age=" + ageSec + " ms=" + System.currentTimeMillis());
Lazy<SearcherTracker> _;
_searchers.TryRemove(tracker.Version, out _);
tracker.Dispose();
}
lastRecordTimeSec = tracker.RecordTimeSec;
}
}
}
/// <summary>
/// Close this to future searching; any searches still in
/// process in other threads won't be affected, and they
/// should still call <see cref="Release(IndexSearcher)"/> after they are
/// done.
///
/// <para/><b>NOTE</b>: you must ensure no other threads are
/// calling <see cref="Record(IndexSearcher)"/> while you call Dispose();
/// otherwise it's possible not all searcher references
/// will be freed.
/// </summary>
public virtual void Dispose()
{
lock (this)
{
_closed = true;
IList<SearcherTracker> toClose = new List<SearcherTracker>(_searchers.Values.Select(item => item.Value));
// Remove up front in case exc below, so we don't
// over-decRef on double-close:
foreach (var tracker in toClose)
{
Lazy<SearcherTracker> _;
_searchers.TryRemove(tracker.Version, out _);
}
IOUtils.Dispose(toClose);
// Make some effort to catch mis-use:
if (_searchers.Count != 0)
{
throw new InvalidOperationException("another thread called record while this SearcherLifetimeManager instance was being closed; not all searchers were closed");
}
}
}
}
}