PageRenderTime 55ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/Raven.Database/Indexing/IndexStorage.cs

https://github.com/kairogyn/ravendb
C# | 1171 lines | 964 code | 176 blank | 31 comment | 131 complexity | 856599aeb79d61060bdbea8890cc89eb MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, BSD-3-Clause, CC-BY-SA-3.0
  1. //-----------------------------------------------------------------------
  2. // <copyright file="IndexStorage.cs" company="Hibernating Rhinos LTD">
  3. // Copyright (c) Hibernating Rhinos LTD. All rights reserved.
  4. // </copyright>
  5. //-----------------------------------------------------------------------
  6. using System;
  7. using System.Collections.Concurrent;
  8. using System.Collections.Generic;
  9. using System.Diagnostics;
  10. using System.Globalization;
  11. using System.IO;
  12. using System.Linq;
  13. using System.Runtime.ConstrainedExecution;
  14. using System.Text;
  15. using System.Threading;
  16. using System.Threading.Tasks;
  17. using Lucene.Net.Analysis;
  18. using Lucene.Net.Analysis.Standard;
  19. using Lucene.Net.Index;
  20. using Lucene.Net.Search;
  21. using Lucene.Net.Store;
  22. using Raven.Abstractions;
  23. using Raven.Abstractions.Data;
  24. using Raven.Abstractions.Extensions;
  25. using Raven.Abstractions.Indexing;
  26. using Raven.Abstractions.Logging;
  27. using Raven.Abstractions.MEF;
  28. using Raven.Database.Config;
  29. using Raven.Database.Data;
  30. using Raven.Database.Extensions;
  31. using Raven.Database.Impl;
  32. using Raven.Database.Linq;
  33. using Raven.Database.Plugins;
  34. using Raven.Database.Queries;
  35. using Raven.Database.Storage;
  36. using Raven.Imports.Newtonsoft.Json;
  37. using Raven.Json.Linq;
  38. using Directory = System.IO.Directory;
  39. using System.ComponentModel.Composition;
  40. namespace Raven.Database.Indexing
  41. {
  42. /// <summary>
  43. /// Thread safe, single instance for the entire application
  44. /// </summary>
  45. public class IndexStorage : CriticalFinalizerObject, IDisposable
  46. {
  47. private readonly DocumentDatabase documentDatabase;
  48. private const string IndexVersion = "2.0.0.1";
  49. private const string MapReduceIndexVersion = "2.5.0.1";
  50. private readonly IndexDefinitionStorage indexDefinitionStorage;
  51. private readonly InMemoryRavenConfiguration configuration;
  52. private readonly string path;
  53. private readonly ConcurrentDictionary<string, Index> indexes = new ConcurrentDictionary<string, Index>(StringComparer.OrdinalIgnoreCase);
  54. private static readonly ILog log = LogManager.GetCurrentClassLogger();
  55. private static readonly ILog startupLog = LogManager.GetLogger(typeof(IndexStorage).FullName + ".Startup");
  56. private readonly Analyzer dummyAnalyzer = new SimpleAnalyzer();
  57. private DateTime latestPersistedQueryTime;
  58. private readonly FileStream crashMarker;
  59. public IndexStorage(IndexDefinitionStorage indexDefinitionStorage, InMemoryRavenConfiguration configuration, DocumentDatabase documentDatabase)
  60. {
  61. try
  62. {
  63. this.indexDefinitionStorage = indexDefinitionStorage;
  64. this.configuration = configuration;
  65. this.documentDatabase = documentDatabase;
  66. path = configuration.IndexStoragePath;
  67. if (Directory.Exists(path) == false && configuration.RunInMemory == false)
  68. Directory.CreateDirectory(path);
  69. if (configuration.RunInMemory == false)
  70. {
  71. var crashMarkerPath = Path.Combine(path, "indexing.crash-marker");
  72. if (File.Exists(crashMarkerPath))
  73. {
  74. // the only way this can happen is if we crashed because of a power outage
  75. // in this case, we consider all open indexes to be corrupt and force them
  76. // to be reset. This is because to get better perf, we don't flush the files to disk,
  77. // so in the case of a power outage, we can't be sure that there wasn't still stuff in
  78. // the OS buffer that wasn't written yet.
  79. configuration.ResetIndexOnUncleanShutdown = true;
  80. }
  81. // The delete on close ensures that the only way this file will exists is if there was
  82. // a power outage while the server was running.
  83. crashMarker = File.Create(crashMarkerPath, 16, FileOptions.DeleteOnClose);
  84. }
  85. BackgroundTaskExecuter.Instance.ExecuteAllInterleaved(documentDatabase.WorkContext,
  86. indexDefinitionStorage.IndexNames, OpenIndexOnStartup);
  87. }
  88. catch
  89. {
  90. Dispose();
  91. throw;
  92. }
  93. }
  94. private void OpenIndexOnStartup(string indexName)
  95. {
  96. if (indexName == null) throw new ArgumentNullException("indexName");
  97. var fixedName = indexDefinitionStorage.FixupIndexName(indexName);
  98. startupLog.Debug("Loading saved index {0}", indexName);
  99. var indexDefinition = indexDefinitionStorage.GetIndexDefinition(fixedName);
  100. if (indexDefinition == null)
  101. return;
  102. Index indexImplementation;
  103. bool resetTried = false;
  104. bool recoveryTried = false;
  105. string[] keysToDeleteAfterRecovery = null;
  106. while (true)
  107. {
  108. Lucene.Net.Store.Directory luceneDirectory = null;
  109. try
  110. {
  111. luceneDirectory = OpenOrCreateLuceneDirectory(indexDefinition, createIfMissing: resetTried);
  112. indexImplementation = CreateIndexImplementation(fixedName, indexDefinition, luceneDirectory);
  113. var simpleIndex = indexImplementation as SimpleIndex; // no need to do this on m/r indexes, since we rebuild them from saved data anyway
  114. if (simpleIndex != null && keysToDeleteAfterRecovery != null)
  115. {
  116. // remove keys from index that were deleted after creating commit point
  117. simpleIndex.RemoveDirectlyFromIndex(keysToDeleteAfterRecovery);
  118. }
  119. LoadExistingSuggestionsExtentions(fixedName, indexImplementation);
  120. documentDatabase.TransactionalStorage.Batch(accessor =>
  121. {
  122. IndexStats indexStats = accessor.Indexing.GetIndexStats(fixedName);
  123. if (indexStats != null)
  124. {
  125. indexImplementation.Priority = indexStats.Priority;
  126. }
  127. var read = accessor.Lists.Read("Raven/Indexes/QueryTime", fixedName);
  128. if (read == null)
  129. {
  130. if(IsIdleAutoIndex(indexImplementation))
  131. indexImplementation.MarkQueried(); // prevent index abandoning right after startup
  132. return;
  133. }
  134. var dateTime = read.Data.Value<DateTime>("LastQueryTime");
  135. if(IsIdleAutoIndex(indexImplementation) && SystemTime.UtcNow - dateTime > configuration.TimeToWaitBeforeRunningAbandonedIndexes)
  136. indexImplementation.MarkQueried(); // prevent index abandoning right after startup
  137. else
  138. indexImplementation.MarkQueried(dateTime);
  139. if (dateTime > latestPersistedQueryTime)
  140. latestPersistedQueryTime = dateTime;
  141. });
  142. break;
  143. }
  144. catch (Exception e)
  145. {
  146. if (resetTried)
  147. throw new InvalidOperationException("Could not open / create index" + indexName + ", reset already tried", e);
  148. if (recoveryTried == false && luceneDirectory != null)
  149. {
  150. recoveryTried = true;
  151. startupLog.WarnException("Could not open index " + indexName + ". Trying to recover index", e);
  152. keysToDeleteAfterRecovery = TryRecoveringIndex(fixedName, indexDefinition, luceneDirectory);
  153. }
  154. else
  155. {
  156. resetTried = true;
  157. startupLog.WarnException("Could not open index " + indexName + ". Recovery operation failed, forcibly resetting index", e);
  158. TryResettingIndex(fixedName, indexDefinition);
  159. }
  160. }
  161. }
  162. indexes.TryAdd(fixedName, indexImplementation);
  163. }
  164. private static bool IsIdleAutoIndex(Index index)
  165. {
  166. return index.name.StartsWith("Auto/") && index.Priority == IndexingPriority.Idle;
  167. }
  168. private void TryResettingIndex(string indexName, IndexDefinition indexDefinition)
  169. {
  170. try
  171. {
  172. documentDatabase.TransactionalStorage.Batch(accessor =>
  173. {
  174. accessor.Indexing.DeleteIndex(indexName, documentDatabase.WorkContext.CancellationToken);
  175. accessor.Indexing.AddIndex(indexName, indexDefinition.IsMapReduce);
  176. });
  177. var indexDirectory = indexName;
  178. var indexFullPath = Path.Combine(path, MonoHttpUtility.UrlEncode(indexDirectory));
  179. IOExtensions.DeleteDirectory(indexFullPath);
  180. var suggestionsForIndex = Path.Combine(configuration.IndexStoragePath, "Raven-Suggestions", indexName);
  181. IOExtensions.DeleteDirectory(suggestionsForIndex);
  182. }
  183. catch (Exception exception)
  184. {
  185. throw new InvalidOperationException("Could not reset index " + indexName, exception);
  186. }
  187. }
  188. private string[] TryRecoveringIndex(string indexName, IndexDefinition indexDefinition,
  189. Lucene.Net.Store.Directory luceneDirectory)
  190. {
  191. string[] keysToDeleteAfterRecovery = null;
  192. if (indexDefinition.IsMapReduce == false)
  193. {
  194. IndexCommitPoint commitUsedToRestore;
  195. if (TryReusePreviousCommitPointsToRecoverIndex(luceneDirectory,
  196. indexDefinition, path,
  197. out commitUsedToRestore,
  198. out keysToDeleteAfterRecovery))
  199. {
  200. ResetLastIndexedEtagAccordingToRestoredCommitPoint(indexDefinition, commitUsedToRestore);
  201. }
  202. }
  203. else
  204. {
  205. RegenerateMapReduceIndex(luceneDirectory, indexDefinition.Name, indexDefinition);
  206. }
  207. return keysToDeleteAfterRecovery;
  208. }
  209. private void LoadExistingSuggestionsExtentions(string indexName, Index indexImplementation)
  210. {
  211. var suggestionsForIndex = Path.Combine(configuration.IndexStoragePath, "Raven-Suggestions", indexName);
  212. if (!Directory.Exists(suggestionsForIndex))
  213. return;
  214. try
  215. {
  216. foreach (var directory in Directory.GetDirectories(suggestionsForIndex))
  217. {
  218. IndexSearcher searcher;
  219. using (indexImplementation.GetSearcher(out searcher))
  220. {
  221. var key = Path.GetFileName(directory);
  222. var decodedKey = MonoHttpUtility.UrlDecode(key);
  223. var lastIndexOfDash = decodedKey.LastIndexOf('-');
  224. var accuracy = float.Parse(decodedKey.Substring(lastIndexOfDash + 1));
  225. var lastIndexOfDistance = decodedKey.LastIndexOf('-', lastIndexOfDash - 1);
  226. StringDistanceTypes distanceType;
  227. Enum.TryParse(decodedKey.Substring(lastIndexOfDistance + 1, lastIndexOfDash - lastIndexOfDistance - 1),
  228. true, out distanceType);
  229. var field = decodedKey.Substring(0, lastIndexOfDistance);
  230. var extension = new SuggestionQueryIndexExtension(
  231. documentDatabase.WorkContext,
  232. Path.Combine(configuration.IndexStoragePath, "Raven-Suggestions", indexName, key), searcher.IndexReader.Directory() is RAMDirectory,
  233. SuggestionQueryRunner.GetStringDistance(distanceType),
  234. field,
  235. accuracy);
  236. indexImplementation.SetExtension(key, extension);
  237. }
  238. }
  239. }
  240. catch (Exception e)
  241. {
  242. log.WarnException("Could not open suggestions for index " + indexName + ", resetting the index", e);
  243. try
  244. {
  245. IOExtensions.DeleteDirectory(suggestionsForIndex);
  246. }
  247. catch (Exception)
  248. {
  249. // ignore the failure
  250. }
  251. throw;
  252. }
  253. }
  254. protected Lucene.Net.Store.Directory OpenOrCreateLuceneDirectory(
  255. IndexDefinition indexDefinition,
  256. string indexName = null,
  257. bool createIfMissing = true)
  258. {
  259. Lucene.Net.Store.Directory directory;
  260. if (configuration.RunInMemory ||
  261. (indexDefinition.IsMapReduce == false && // there is no point in creating map/reduce indexes in memory, we write the intermediate results to disk anyway
  262. indexDefinitionStorage.IsNewThisSession(indexDefinition) &&
  263. indexDefinition.DisableInMemoryIndexing == false &&
  264. configuration.DisableInMemoryIndexing == false))
  265. {
  266. directory = new RAMDirectory();
  267. new IndexWriter(directory, dummyAnalyzer, IndexWriter.MaxFieldLength.UNLIMITED).Dispose(); // creating index structure
  268. }
  269. else
  270. {
  271. var indexDirectory = indexName ?? IndexDefinitionStorage.FixupIndexName(indexDefinition.Name, path);
  272. var indexFullPath = Path.Combine(path, MonoHttpUtility.UrlEncode(indexDirectory));
  273. directory = new LuceneCodecDirectory(indexFullPath, documentDatabase.IndexCodecs.OfType<AbstractIndexCodec>());
  274. if (!IndexReader.IndexExists(directory))
  275. {
  276. if (createIfMissing == false)
  277. throw new InvalidOperationException("Index does not exists: " + indexDirectory);
  278. WriteIndexVersion(directory, indexDefinition);
  279. //creating index structure if we need to
  280. new IndexWriter(directory, dummyAnalyzer, IndexWriter.MaxFieldLength.UNLIMITED).Dispose();
  281. }
  282. else
  283. {
  284. EnsureIndexVersionMatches(indexName, directory, indexDefinition);
  285. if (directory.FileExists("write.lock"))// force lock release, because it was still open when we shut down
  286. {
  287. IndexWriter.Unlock(directory);
  288. // for some reason, just calling unlock doesn't remove this file
  289. directory.DeleteFile("write.lock");
  290. }
  291. if (directory.FileExists("writing-to-index.lock")) // we had an unclean shutdown
  292. {
  293. if (configuration.ResetIndexOnUncleanShutdown)
  294. throw new InvalidOperationException("Rude shutdown detected on: " + indexDirectory);
  295. CheckIndexAndTryToFix(directory, indexDirectory);
  296. directory.DeleteFile("writing-to-index.lock");
  297. }
  298. }
  299. }
  300. return directory;
  301. }
  302. private void RegenerateMapReduceIndex(Lucene.Net.Store.Directory directory, string indexName, IndexDefinition indexDefinition)
  303. {
  304. // remove old index data
  305. var dirOnDisk = Path.Combine(path, MonoHttpUtility.UrlEncode(indexName));
  306. IOExtensions.DeleteDirectory(dirOnDisk);
  307. // initialize by new index
  308. Directory.CreateDirectory(dirOnDisk);
  309. WriteIndexVersion(directory, indexDefinition);
  310. new IndexWriter(directory, dummyAnalyzer, IndexWriter.MaxFieldLength.UNLIMITED).Dispose();
  311. var start = 0;
  312. const int take = 100;
  313. documentDatabase.TransactionalStorage.Batch(actions =>
  314. {
  315. IList<ReduceTypePerKey> reduceKeysAndTypes;
  316. do
  317. {
  318. reduceKeysAndTypes = actions.MapReduce.GetReduceKeysAndTypes(indexName, start, take).ToList();
  319. start += take;
  320. var keysToScheduleOnLevel2 =
  321. reduceKeysAndTypes.Where(x => x.OperationTypeToPerform == ReduceType.MultiStep).ToList();
  322. var keysToScheduleOnLevel0 =
  323. reduceKeysAndTypes.Where(x => x.OperationTypeToPerform == ReduceType.SingleStep).ToList();
  324. var itemsToScheduleOnLevel2 = keysToScheduleOnLevel2.Select(x => new ReduceKeyAndBucket(0, x.ReduceKey)).ToList();
  325. var itemsToScheduleOnLevel0 = new List<ReduceKeyAndBucket>();
  326. foreach (var reduceKey in keysToScheduleOnLevel0.Select(x => x.ReduceKey))
  327. {
  328. var mappedBuckets = actions.MapReduce.GetMappedBuckets(indexName, reduceKey).Distinct();
  329. itemsToScheduleOnLevel0.AddRange(mappedBuckets.Select(x => new ReduceKeyAndBucket(x, reduceKey)));
  330. actions.General.MaybePulseTransaction();
  331. }
  332. foreach (var itemToReduce in itemsToScheduleOnLevel2)
  333. {
  334. actions.MapReduce.ScheduleReductions(indexName, 2, itemToReduce);
  335. actions.General.MaybePulseTransaction();
  336. }
  337. foreach (var itemToReduce in itemsToScheduleOnLevel0)
  338. {
  339. actions.MapReduce.ScheduleReductions(indexName, 0, itemToReduce);
  340. actions.General.MaybePulseTransaction();
  341. }
  342. } while (reduceKeysAndTypes.Count > 0);
  343. });
  344. }
  345. private void ResetLastIndexedEtagAccordingToRestoredCommitPoint(IndexDefinition indexDefinition,
  346. IndexCommitPoint lastCommitPoint)
  347. {
  348. documentDatabase.TransactionalStorage.Batch(
  349. accessor =>
  350. accessor.Indexing.UpdateLastIndexed(indexDefinition.Name, lastCommitPoint.HighestCommitedETag,
  351. lastCommitPoint.TimeStamp));
  352. }
  353. public static string IndexVersionFileName(IndexDefinition indexDefinition)
  354. {
  355. if (indexDefinition.IsMapReduce)
  356. return "mapReduce.version";
  357. return "index.version";
  358. }
  359. public static void WriteIndexVersion(Lucene.Net.Store.Directory directory, IndexDefinition indexDefinition)
  360. {
  361. var version = IndexVersion;
  362. if(indexDefinition.IsMapReduce)
  363. {
  364. version = MapReduceIndexVersion;
  365. }
  366. using (var indexOutput = directory.CreateOutput(IndexVersionFileName(indexDefinition)))
  367. {
  368. indexOutput.WriteString(version);
  369. indexOutput.Flush();
  370. }
  371. }
  372. private static void EnsureIndexVersionMatches(string indexName, Lucene.Net.Store.Directory directory, IndexDefinition indexDefinition)
  373. {
  374. var versionToCheck = IndexVersion;
  375. if(indexDefinition.IsMapReduce)
  376. {
  377. versionToCheck = MapReduceIndexVersion;
  378. }
  379. var indexVersion = IndexVersionFileName(indexDefinition);
  380. if (directory.FileExists(indexVersion) == false)
  381. {
  382. throw new InvalidOperationException("Could not find " + indexVersion + " " + indexName + ", resetting index");
  383. }
  384. using (var indexInput = directory.OpenInput(indexVersion))
  385. {
  386. var versionFromDisk = indexInput.ReadString();
  387. if (versionFromDisk != versionToCheck)
  388. throw new InvalidOperationException("Index " + indexName + " is of version " + versionFromDisk +
  389. " which is not compatible with " + versionToCheck + ", resetting index");
  390. }
  391. }
  392. private static void CheckIndexAndTryToFix(Lucene.Net.Store.Directory directory, string indexDirectory)
  393. {
  394. startupLog.Warn("Unclean shutdown detected on {0}, checking the index for errors. This may take a while.", indexDirectory);
  395. var memoryStream = new MemoryStream();
  396. var stringWriter = new StreamWriter(memoryStream);
  397. var checkIndex = new CheckIndex(directory);
  398. if (startupLog.IsWarnEnabled)
  399. checkIndex.SetInfoStream(stringWriter);
  400. var sp = Stopwatch.StartNew();
  401. var status = checkIndex.CheckIndex_Renamed_Method();
  402. sp.Stop();
  403. if (startupLog.IsWarnEnabled)
  404. {
  405. startupLog.Warn("Checking index {0} took: {1}, clean: {2}", indexDirectory, sp.Elapsed, status.clean);
  406. memoryStream.Position = 0;
  407. log.Warn(new StreamReader(memoryStream).ReadToEnd());
  408. }
  409. if (status.clean)
  410. return;
  411. startupLog.Warn("Attempting to fix index: {0}", indexDirectory);
  412. sp.Restart();
  413. checkIndex.FixIndex(status);
  414. startupLog.Warn("Fixed index {0} in {1}", indexDirectory, sp.Elapsed);
  415. }
  416. public void StoreCommitPoint(string indexName, IndexCommitPoint indexCommit)
  417. {
  418. if (indexCommit.SegmentsInfo == null || indexCommit.SegmentsInfo.IsIndexCorrupted)
  419. return;
  420. var directoryName = indexCommit.SegmentsInfo.Generation.ToString("0000000000000000000", CultureInfo.InvariantCulture);
  421. var commitPointDirectory = new IndexCommitPointDirectory(path, indexName, directoryName);
  422. if (Directory.Exists(commitPointDirectory.AllCommitPointsFullPath) == false)
  423. {
  424. Directory.CreateDirectory(commitPointDirectory.AllCommitPointsFullPath);
  425. }
  426. Directory.CreateDirectory(commitPointDirectory.FullPath);
  427. using (var commitPointFile = File.Create(commitPointDirectory.FileFullPath))
  428. {
  429. using (var sw = new StreamWriter(commitPointFile))
  430. {
  431. var jsonSerializer = new JsonSerializer();
  432. var textWriter = new JsonTextWriter(sw);
  433. jsonSerializer.Serialize(textWriter, indexCommit);
  434. sw.Flush();
  435. }
  436. }
  437. var currentSegmentsFileName = indexCommit.SegmentsInfo.SegmentsFileName;
  438. File.Copy(Path.Combine(commitPointDirectory.IndexFullPath, currentSegmentsFileName),
  439. Path.Combine(commitPointDirectory.FullPath, currentSegmentsFileName),
  440. overwrite: true);
  441. var storedCommitPoints = Directory.GetDirectories(commitPointDirectory.AllCommitPointsFullPath);
  442. if (storedCommitPoints.Length > configuration.MaxNumberOfStoredCommitPoints)
  443. {
  444. foreach (var toDelete in storedCommitPoints.Take(storedCommitPoints.Length - configuration.MaxNumberOfStoredCommitPoints))
  445. {
  446. IOExtensions.DeleteDirectory(toDelete);
  447. }
  448. }
  449. }
  450. public void AddDeletedKeysToCommitPoints(string indexName, string[] deletedKeys)
  451. {
  452. var indexFullPath = Path.Combine(path, MonoHttpUtility.UrlEncode(indexName));
  453. var existingCommitPoints = IndexCommitPointDirectory.ScanAllCommitPointsDirectory(indexFullPath);
  454. foreach (var commitPointDirectory in existingCommitPoints.Select(commitPoint => new IndexCommitPointDirectory(path, indexName, commitPoint)))
  455. {
  456. using (var stream = File.Open(commitPointDirectory.DeletedKeysFile, FileMode.OpenOrCreate))
  457. {
  458. stream.Seek(0, SeekOrigin.End);
  459. using (var writer = new StreamWriter(stream))
  460. {
  461. foreach (var deletedKey in deletedKeys)
  462. {
  463. writer.WriteLine(deletedKey);
  464. }
  465. }
  466. }
  467. }
  468. }
  469. private static bool TryReusePreviousCommitPointsToRecoverIndex(Lucene.Net.Store.Directory directory, IndexDefinition indexDefinition, string indexStoragePath, out IndexCommitPoint indexCommit, out string[] keysToDelete)
  470. {
  471. indexCommit = null;
  472. keysToDelete = null;
  473. if (indexDefinition.IsMapReduce)
  474. return false;
  475. var indexFullPath = Path.Combine(indexStoragePath, MonoHttpUtility.UrlEncode(indexDefinition.Name));
  476. var allCommitPointsFullPath = IndexCommitPointDirectory.GetAllCommitPointsFullPath(indexFullPath);
  477. if (Directory.Exists(allCommitPointsFullPath) == false)
  478. return false;
  479. var filesInIndexDirectory = Directory.GetFiles(indexFullPath).Select(Path.GetFileName);
  480. var existingCommitPoints =
  481. IndexCommitPointDirectory.ScanAllCommitPointsDirectory(indexFullPath);
  482. Array.Reverse(existingCommitPoints); // start from the highest generation
  483. foreach (var commitPointDirectoryName in existingCommitPoints)
  484. {
  485. try
  486. {
  487. var commitPointDirectory = new IndexCommitPointDirectory(indexStoragePath, indexDefinition.Name,
  488. commitPointDirectoryName);
  489. using (var commitPointFile = File.Open(commitPointDirectory.FileFullPath, FileMode.Open))
  490. {
  491. var jsonSerializer = new JsonSerializer();
  492. var textReader = new JsonTextReader(new StreamReader(commitPointFile));
  493. indexCommit = jsonSerializer.Deserialize<IndexCommitPoint>(textReader);
  494. }
  495. var missingFile =
  496. indexCommit.SegmentsInfo.ReferencedFiles.Any(
  497. referencedFile => filesInIndexDirectory.Contains(referencedFile) == false);
  498. if (missingFile)
  499. {
  500. IOExtensions.DeleteDirectory(commitPointDirectory.FullPath);
  501. continue; // there are some missing files, try another commit point
  502. }
  503. var storedSegmentsFile = indexCommit.SegmentsInfo.SegmentsFileName;
  504. // here there should be only one segments_N file, however remove all if there is more
  505. foreach (var currentSegmentsFile in Directory.GetFiles(commitPointDirectory.IndexFullPath, "segments_*"))
  506. {
  507. File.Delete(currentSegmentsFile);
  508. }
  509. // copy old segments_N file
  510. File.Copy(Path.Combine(commitPointDirectory.FullPath, storedSegmentsFile),
  511. Path.Combine(commitPointDirectory.IndexFullPath, storedSegmentsFile), true);
  512. try
  513. {
  514. // update segments.gen file
  515. using (var genOutput = directory.CreateOutput(IndexFileNames.SEGMENTS_GEN))
  516. {
  517. genOutput.WriteInt(SegmentInfos.FORMAT_LOCKLESS);
  518. genOutput.WriteLong(indexCommit.SegmentsInfo.Generation);
  519. genOutput.WriteLong(indexCommit.SegmentsInfo.Generation);
  520. }
  521. }
  522. catch (Exception)
  523. {
  524. // here we can ignore, segments.gen is used only as fallback
  525. }
  526. if (File.Exists(commitPointDirectory.DeletedKeysFile))
  527. keysToDelete = File.ReadLines(commitPointDirectory.DeletedKeysFile).ToArray();
  528. return true;
  529. }
  530. catch (Exception ex)
  531. {
  532. startupLog.WarnException("Could not recover an index named '" + indexDefinition.Name +
  533. "'from segments of the following generation " + commitPointDirectoryName, ex);
  534. }
  535. }
  536. return false;
  537. }
  538. internal Lucene.Net.Store.Directory MakeRAMDirectoryPhysical(RAMDirectory ramDir, string indexName)
  539. {
  540. var newDir = new LuceneCodecDirectory(Path.Combine(path, MonoHttpUtility.UrlEncode(IndexDefinitionStorage.FixupIndexName(indexName, path))), documentDatabase.IndexCodecs.OfType<AbstractIndexCodec>());
  541. Lucene.Net.Store.Directory.Copy(ramDir, newDir, false);
  542. return newDir;
  543. }
  544. private Index CreateIndexImplementation(string directoryPath, IndexDefinition indexDefinition, Lucene.Net.Store.Directory directory)
  545. {
  546. var viewGenerator = indexDefinitionStorage.GetViewGenerator(indexDefinition.Name);
  547. var indexImplementation = indexDefinition.IsMapReduce
  548. ? (Index)new MapReduceIndex(directory, directoryPath, indexDefinition, viewGenerator, documentDatabase.WorkContext)
  549. : new SimpleIndex(directory, directoryPath, indexDefinition, viewGenerator, documentDatabase.WorkContext);
  550. configuration.Container.SatisfyImportsOnce(indexImplementation);
  551. return indexImplementation;
  552. }
  553. public string[] Indexes
  554. {
  555. get { return indexes.Keys.ToArray(); }
  556. }
  557. public bool HasIndex(string index)
  558. {
  559. if (index == null)
  560. return false;
  561. return indexes.ContainsKey(index);
  562. }
  563. public void Dispose()
  564. {
  565. var exceptionAggregator = new ExceptionAggregator(log, "Could not properly close index storage");
  566. exceptionAggregator.Execute(() => Parallel.ForEach(indexes.Values, index => exceptionAggregator.Execute(index.Dispose)));
  567. exceptionAggregator.Execute(() => dummyAnalyzer.Close());
  568. exceptionAggregator.Execute(() =>
  569. {
  570. if (crashMarker != null)
  571. crashMarker.Dispose();
  572. });
  573. exceptionAggregator.ThrowIfNeeded();
  574. }
  575. public void DeleteIndex(string name)
  576. {
  577. Index value;
  578. if (indexes.TryGetValue(name, out value) == false)
  579. {
  580. log.Debug("Ignoring delete for non existing index {0}", name);
  581. return;
  582. }
  583. log.Debug("Deleting index {0}", name);
  584. value.Dispose();
  585. Index ignored;
  586. var dirOnDisk = Path.Combine(path, MonoHttpUtility.UrlEncode(name));
  587. documentDatabase.TransactionalStorage.Batch(accessor =>
  588. accessor.Lists.Remove("Raven/Indexes/QueryTime", name));
  589. if (!indexes.TryRemove(name, out ignored) || !Directory.Exists(dirOnDisk))
  590. return;
  591. IOExtensions.DeleteDirectory(dirOnDisk);
  592. }
  593. public void CreateIndexImplementation(IndexDefinition indexDefinition)
  594. {
  595. var fixedName = indexDefinitionStorage.FixupIndexName(indexDefinition.Name);
  596. log.Debug("Creating index {0} with encoded name {1}", indexDefinition.Name, fixedName);
  597. IndexDefinitionStorage.ResolveAnalyzers(indexDefinition);
  598. AssertAnalyzersValid(indexDefinition);
  599. indexes.AddOrUpdate(fixedName, n =>
  600. {
  601. var directory = OpenOrCreateLuceneDirectory(indexDefinition, fixedName);
  602. return CreateIndexImplementation(fixedName, indexDefinition, directory);
  603. }, (s, index) => index);
  604. }
  605. private static void AssertAnalyzersValid(IndexDefinition indexDefinition)
  606. {
  607. foreach (var analyzer in from analyzer in indexDefinition.Analyzers
  608. let analyzerType = typeof(StandardAnalyzer).Assembly.GetType(analyzer.Value) ?? Type.GetType(analyzer.Value, throwOnError: false)
  609. where analyzerType == null
  610. select analyzer)
  611. {
  612. throw new ArgumentException(string.Format("Could not create analyzer for field: '{0}' because the type '{1}' was not found", analyzer.Key, analyzer.Value));
  613. }
  614. }
  615. public Query GetLuceneQuery(string index, IndexQuery query, OrderedPartCollection<AbstractIndexQueryTrigger> indexQueryTriggers)
  616. {
  617. Index value;
  618. if (indexes.TryGetValue(index, out value) == false)
  619. {
  620. log.Debug("Query on non existing index {0}", index);
  621. throw new InvalidOperationException("Index '" + index + "' does not exists");
  622. }
  623. var fieldsToFetch = new FieldsToFetch(new string[0], AggregationOperation.None, null);
  624. return new Index.IndexQueryOperation(value, query, _ => false, fieldsToFetch, indexQueryTriggers).GetLuceneQuery();
  625. }
  626. public IEnumerable<IndexQueryResult> Query(string index, IndexQuery query, Func<IndexQueryResult, bool> shouldIncludeInResults, FieldsToFetch fieldsToFetch, OrderedPartCollection<AbstractIndexQueryTrigger> indexQueryTriggers, CancellationToken token)
  627. {
  628. Index value;
  629. if (indexes.TryGetValue(index, out value) == false)
  630. {
  631. log.Debug("Query on non existing index '{0}'", index);
  632. throw new InvalidOperationException("Index '" + index + "' does not exists");
  633. }
  634. if ((value.Priority.HasFlag(IndexingPriority.Idle) || value.Priority.HasFlag(IndexingPriority.Abandoned)) &&
  635. value.Priority.HasFlag(IndexingPriority.Forced) == false)
  636. {
  637. documentDatabase.TransactionalStorage.Batch(accessor =>
  638. {
  639. value.Priority = IndexingPriority.Normal;
  640. try
  641. {
  642. accessor.Indexing.SetIndexPriority(index, IndexingPriority.Normal);
  643. }
  644. catch (Exception e)
  645. {
  646. if (accessor.IsWriteConflict(e) == false)
  647. throw;
  648. // we explciitly ignore write conflicts here, it is okay if we got set twice (two concurrent queries, or setting while indexing).
  649. }
  650. documentDatabase.WorkContext.ShouldNotifyAboutWork(() => "Idle index queried");
  651. documentDatabase.RaiseNotifications(new IndexChangeNotification()
  652. {
  653. Name = value.name,
  654. Type = IndexChangeTypes.IndexPromotedFromIdle
  655. });
  656. });
  657. }
  658. var indexQueryOperation = new Index.IndexQueryOperation(value, query, shouldIncludeInResults, fieldsToFetch, indexQueryTriggers);
  659. if (query.Query != null && query.Query.Contains(Constants.IntersectSeparator))
  660. return indexQueryOperation.IntersectionQuery(token);
  661. return indexQueryOperation.Query(token);
  662. }
  663. public IEnumerable<RavenJObject> IndexEntires(
  664. string index,
  665. IndexQuery query,
  666. OrderedPartCollection<AbstractIndexQueryTrigger> indexQueryTriggers,
  667. Reference<int> totalResults)
  668. {
  669. Index value;
  670. if (indexes.TryGetValue(index, out value) == false)
  671. {
  672. log.Debug("Query on non existing index '{0}'", index);
  673. throw new InvalidOperationException("Index '" + index + "' does not exists");
  674. }
  675. var indexQueryOperation = new Index.IndexQueryOperation(value, query, null, new FieldsToFetch(null, AggregationOperation.None, null), indexQueryTriggers);
  676. return indexQueryOperation.IndexEntries(totalResults);
  677. }
  678. protected internal static IDisposable EnsureInvariantCulture()
  679. {
  680. if (Thread.CurrentThread.CurrentCulture == CultureInfo.InvariantCulture)
  681. return null;
  682. var oldCurrentCulture = Thread.CurrentThread.CurrentCulture;
  683. var oldCurrentUiCulture = Thread.CurrentThread.CurrentUICulture;
  684. Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
  685. Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
  686. return new DisposableAction(() =>
  687. {
  688. Thread.CurrentThread.CurrentCulture = oldCurrentCulture;
  689. Thread.CurrentThread.CurrentUICulture = oldCurrentUiCulture;
  690. });
  691. }
  692. public void RemoveFromIndex(string index, string[] keys, WorkContext context)
  693. {
  694. Index value;
  695. if (indexes.TryGetValue(index, out value) == false)
  696. {
  697. log.Debug("Removing from non existing index '{0}', ignoring", index);
  698. return;
  699. }
  700. value.Remove(keys, context);
  701. context.RaiseIndexChangeNotification(new IndexChangeNotification
  702. {
  703. Name = index,
  704. Type = IndexChangeTypes.RemoveFromIndex
  705. });
  706. }
  707. public void Index(string index,
  708. AbstractViewGenerator viewGenerator,
  709. IndexingBatch batch,
  710. WorkContext context,
  711. IStorageActionsAccessor actions,
  712. DateTime minimumTimestamp)
  713. {
  714. Index value;
  715. if (indexes.TryGetValue(index, out value) == false)
  716. {
  717. log.Debug("Tried to index on a non existent index {0}, ignoring", index);
  718. return;
  719. }
  720. using (EnsureInvariantCulture())
  721. using (DocumentCacher.SkipSettingDocumentsInDocumentCache())
  722. {
  723. value.IndexDocuments(viewGenerator, batch, actions, minimumTimestamp);
  724. context.RaiseIndexChangeNotification(new IndexChangeNotification
  725. {
  726. Name = index,
  727. Type = IndexChangeTypes.MapCompleted
  728. });
  729. }
  730. }
  731. public void Reduce(
  732. string index,
  733. AbstractViewGenerator viewGenerator,
  734. IEnumerable<IGrouping<int, object>> mappedResults,
  735. int level,
  736. WorkContext context,
  737. IStorageActionsAccessor actions,
  738. HashSet<string> reduceKeys,
  739. int inputCount)
  740. {
  741. Index value;
  742. if (indexes.TryGetValue(index, out value) == false)
  743. {
  744. log.Debug("Tried to index on a non existent index {0}, ignoring", index);
  745. return;
  746. }
  747. var mapReduceIndex = value as MapReduceIndex;
  748. if (mapReduceIndex == null)
  749. {
  750. log.Warn("Tried to reduce on an index that is not a map/reduce index: {0}, ignoring", index);
  751. return;
  752. }
  753. using (EnsureInvariantCulture())
  754. {
  755. var reduceDocuments = new MapReduceIndex.ReduceDocuments(mapReduceIndex, viewGenerator, mappedResults, level, context, actions, reduceKeys, inputCount);
  756. reduceDocuments.ExecuteReduction();
  757. context.RaiseIndexChangeNotification(new IndexChangeNotification
  758. {
  759. Name = index,
  760. Type = IndexChangeTypes.ReduceCompleted
  761. });
  762. }
  763. }
  764. internal IndexSearcherHolder.IndexSearcherHoldingState GetCurrentStateHolder(string indexName)
  765. {
  766. return GetIndexByName(indexName).GetCurrentStateHolder();
  767. }
  768. public IDisposable GetCurrentIndexSearcher(string indexName, out IndexSearcher searcher)
  769. {
  770. return GetIndexByName(indexName).GetSearcher(out searcher);
  771. }
  772. public IDisposable GetCurrentIndexSearcherAndTermDocs(string indexName, out IndexSearcher searcher, out RavenJObject[] termsDocs)
  773. {
  774. return GetIndexByName(indexName).GetSearcherAndTermsDocs(out searcher, out termsDocs);
  775. }
  776. private Index GetIndexByName(string indexName)
  777. {
  778. var result = indexes.Where(index => String.Compare(index.Key, indexName, StringComparison.OrdinalIgnoreCase) == 0)
  779. .Select(x => x.Value)
  780. .FirstOrDefault();
  781. if (result == null)
  782. throw new InvalidOperationException(string.Format("Index '{0}' does not exist", indexName));
  783. return result;
  784. }
  785. public void RunIdleOperations()
  786. {
  787. foreach (var value in indexes.Values)
  788. {
  789. if ((SystemTime.UtcNow - value.LastIndexTime).TotalMinutes < 1)
  790. continue;
  791. value.Flush();
  792. }
  793. SetUnusedIndexesToIdle();
  794. UpdateLatestPersistedQueryTime();
  795. }
  796. private void UpdateLatestPersistedQueryTime()
  797. {
  798. documentDatabase.TransactionalStorage.Batch(accessor =>
  799. {
  800. var maxDate = latestPersistedQueryTime;
  801. foreach (var index in indexes)
  802. {
  803. var lastQueryTime = index.Value.LastQueryTime ?? DateTime.MinValue;
  804. if (lastQueryTime <= latestPersistedQueryTime)
  805. continue;
  806. accessor.Lists.Set("Raven/Indexes/QueryTime", index.Key, new RavenJObject
  807. {
  808. {"LastQueryTime", lastQueryTime}
  809. }, UuidType.Indexing);
  810. if (lastQueryTime > maxDate)
  811. maxDate = lastQueryTime;
  812. }
  813. latestPersistedQueryTime = maxDate;
  814. });
  815. }
  816. public class UnusedIndexState
  817. {
  818. public DateTime LastQueryTime { get; set; }
  819. public Index Index { get; set; }
  820. public string Name { get; set; }
  821. public IndexingPriority Priority { get; set; }
  822. public DateTime CreationDate { get; set; }
  823. }
  824. private void SetUnusedIndexesToIdle()
  825. {
  826. documentDatabase.TransactionalStorage.Batch(accessor =>
  827. {
  828. var autoIndexesSortedByLastQueryTime =
  829. (from index in indexes
  830. let stats = GetIndexStats(accessor, index.Key)
  831. where stats != null
  832. let lastQueryTime = stats.LastQueryTimestamp ?? DateTime.MinValue
  833. where index.Key.StartsWith("Auto/", StringComparison.InvariantCultureIgnoreCase)
  834. orderby lastQueryTime
  835. select new UnusedIndexState
  836. {
  837. LastQueryTime = lastQueryTime,
  838. Index = index.Value,
  839. Name = index.Key,
  840. Priority = stats.Priority,
  841. CreationDate = stats.CreatedTimestamp
  842. }).ToArray();
  843. var idleChecks = 0;
  844. for (var i = 0; i < autoIndexesSortedByLastQueryTime.Length; i++)
  845. {
  846. var thisItem = autoIndexesSortedByLastQueryTime[i];
  847. if (thisItem.Priority.HasFlag(IndexingPriority.Disabled) || // we don't really have much to say about those in here
  848. thisItem.Priority.HasFlag(IndexingPriority.Forced))// if it is forced, we can't change it
  849. continue;
  850. var age = (SystemTime.UtcNow - thisItem.CreationDate).TotalMinutes;
  851. var lastQuery = (SystemTime.UtcNow - thisItem.LastQueryTime).TotalMinutes;
  852. if (thisItem.Priority.HasFlag(IndexingPriority.Normal))
  853. {
  854. var timeToWaitBeforeMarkingAutoIndexAsIdle = documentDatabase.Configuration.TimeToWaitBeforeMarkingAutoIndexAsIdle;
  855. var timeToWaitForIdleMinutes = timeToWaitBeforeMarkingAutoIndexAsIdle.TotalMinutes * 10;
  856. if (age < timeToWaitForIdleMinutes)
  857. {
  858. HandleActiveIndex(thisItem, age, lastQuery, accessor, timeToWaitForIdleMinutes);
  859. }
  860. else if (++idleChecks < 2)
  861. {
  862. // If it's a fairly established query then we need to determine whether there is any activity currently
  863. // If there is activity and this has not been queried against 'recently' it needs idling
  864. if (i < autoIndexesSortedByLastQueryTime.Length - 1)
  865. {
  866. var nextItem = autoIndexesSortedByLastQueryTime[i + 1];
  867. if ((nextItem.LastQueryTime - thisItem.LastQueryTime).TotalMinutes > timeToWaitForIdleMinutes)
  868. {
  869. accessor.Indexing.SetIndexPriority(thisItem.Name, IndexingPriority.Idle);
  870. thisItem.Index.Priority = IndexingPriority.Idle;
  871. documentDatabase.RaiseNotifications(new IndexChangeNotification()
  872. {
  873. Name = thisItem.Name,
  874. Type = IndexChangeTypes.IndexDemotedToIdle
  875. });
  876. }
  877. }
  878. }
  879. continue;
  880. }
  881. if (thisItem.Priority.HasFlag(IndexingPriority.Idle))
  882. {
  883. HandleIdleIndex(age, lastQuery, thisItem, accessor);
  884. continue;
  885. }
  886. }
  887. });
  888. }
  889. private IndexStats GetIndexStats(IStorageActionsAccessor accessor, string indexName)
  890. {
  891. var indexStats = accessor.Indexing.GetIndexStats(indexName);
  892. if (indexStats == null)
  893. return null;
  894. indexStats.LastQueryTimestamp = GetLastQueryTime(indexName);
  895. return indexStats;
  896. }
  897. private void HandleIdleIndex(double age, double lastQuery, UnusedIndexState thisItem,
  898. IStorageActionsAccessor accessor)
  899. {
  900. // relatively young index, haven't been queried for a while already
  901. // can be safely removed, probably
  902. if (age < 90 && lastQuery > 30)
  903. {
  904. documentDatabase.DeleteIndex(thisItem.Name);
  905. return;
  906. }
  907. if (lastQuery < configuration.TimeToWaitBeforeMarkingIdleIndexAsAbandoned.TotalMinutes)
  908. return;
  909. // old enough, and haven't been queried for a while, mark it as abandoned
  910. accessor.Indexing.SetIndexPriority(thisItem.Name, IndexingPriority.Abandoned);
  911. thisItem.Index.Priority = IndexingPriority.Abandoned;
  912. documentDatabase.RaiseNotifications(new IndexChangeNotification()
  913. {
  914. Name = thisItem.Name,
  915. Type = IndexChangeTypes.IndexDemotedToIdle
  916. });
  917. }
  918. private void HandleActiveIndex(UnusedIndexState thisItem, double age, double lastQuery, IStorageActionsAccessor accessor, double timeToWaitForIdle)
  919. {
  920. if (age < (timeToWaitForIdle * 2.5) && lastQuery < (1.5 * timeToWaitForIdle))
  921. return;
  922. if (age < (timeToWaitForIdle * 6) && lastQuery < (2.5 * timeToWaitForIdle))
  923. return;
  924. accessor.Indexing.SetIndexPriority(thisItem.Name, IndexingPriority.Idle);
  925. thisItem.Index.Priority = IndexingPriority.Idle;
  926. documentDatabase.RaiseNotifications(new IndexChangeNotification()
  927. {
  928. Name = thisItem.Name,
  929. Type = IndexChangeTypes.IndexDemotedToIdle
  930. });
  931. }
  932. public void FlushMapIndexes()
  933. {
  934. foreach (var value in indexes.Values.Where(value => !value.IsMapReduce))
  935. {
  936. value.Flush();
  937. }
  938. }
  939. public void FlushReduceIndexes()
  940. {
  941. foreach (var value in indexes.Values.Where(value => value.IsMapReduce))
  942. {
  943. value.Flush();
  944. }
  945. }
  946. public IIndexExtension GetIndexExtension(string index, string indexExtensionKey)
  947. {
  948. return GetIndexByName(index).GetExtension(indexExtensionKey);
  949. }
  950. public IIndexExtension GetIndexExtensionByPrefix(string index, string indexExtensionKeyPrefix)
  951. {
  952. return GetIndexByName(index).GetExtensionByPrefix(indexExtensionKeyPrefix);
  953. }
  954. public void SetIndexExtension(string indexName, string indexExtensionKey, IIndexExtension suggestionQueryIndexExtension)
  955. {
  956. GetIndexByName(indexName).SetExtension(indexExtensionKey, suggestionQueryIndexExtension);
  957. }
  958. public Index GetIndexInstance(string indexName)
  959. {
  960. return indexes.Where(index => String.Compare(index.Key, indexName, StringComparison.OrdinalIgnoreCase) == 0)
  961. .Select(x => x.Value)
  962. .FirstOrDefault();
  963. }
  964. public void MarkCachedQuery(string indexName)
  965. {
  966. GetIndexByName(indexName).MarkQueried();
  967. }
  968. public DateTime? GetLastQueryTime(string index)
  969. {
  970. return GetIndexByName(index).LastQueryTime;
  971. }
  972. public IndexingPerformanceStats[] GetIndexingPerformance(string index)
  973. {
  974. return GetIndexByName(index).GetIndexingPerformance();
  975. }
  976. public void Backup(string directory, string incrementalTag = null)
  977. {
  978. Parallel.ForEach(indexes.Values, index =>
  979. index.Backup(directory, path, incrementalTag));
  980. }
  981. public void MergeAllIndexes()
  982. {
  983. Parallel.ForEach(indexes.Values, index =>
  984. index.MergeSegments());
  985. }
  986. public string IndexOnRam(string name)
  987. {
  988. return GetIndexByName(name).IsOnRam;
  989. }
  990. public void ForceWriteToDisk(string index)
  991. {
  992. GetIndexByName(index).ForceWriteToDisk();
  993. }
  994. }
  995. }