Skip to content

Commit 0e9f695

Browse files
Fixed max age and down-sampling scoping to non-attached paths, downsampling range resolution, and add directory fill method setting
1 parent 26d6103 commit 0e9f695

8 files changed

Lines changed: 220 additions & 63 deletions

File tree

Source/Libraries/Adapters/openHistorian.Adapters/LocalOutputAdapter.cs

Lines changed: 89 additions & 40 deletions
Large diffs are not rendered by default.

Source/Libraries/GSF.SortedTreeStore/GSF.SortedTreeStore.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@
204204
<Compile Include="Snap\Filters\PointIdMatchFilterDefinition.cs" />
205205
<Compile Include="Snap\UnionTreeStreamSortHelper.cs" />
206206
<Compile Include="Snap\Services\ArchiveDetails.cs" />
207+
<Compile Include="Snap\Services\ArchiveDirectoryFillMethod.cs" />
207208
<Compile Include="Snap\Services\ArchiveDirectoryMethod.cs" />
208209
<Compile Include="Snap\Services\ArchiveListEditor.cs" />
209210
<Compile Include="Snap\Services\ArchiveListSettings.cs" />
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//******************************************************************************************************
2+
// ArchiveDirectoryFillMethod.cs - Gbtc
3+
//
4+
// Copyright © 2026, Grid Protection Alliance. All Rights Reserved.
5+
//
6+
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
9+
// not use this file except in compliance with the License. You may obtain a copy of the License at:
10+
//
11+
// http://opensource.org/licenses/MIT
12+
//
13+
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+
// License for the specific language governing permissions and limitations.
16+
//
17+
// Code Modification History:
18+
// ----------------------------------------------------------------------------------------------------
19+
// 06/23/2026 - J. Ritchie Carroll
20+
// Generated original version of source code.
21+
//
22+
//******************************************************************************************************
23+
24+
namespace GSF.Snap.Services
25+
{
26+
/// <summary>
27+
/// Specifies how a write directory is selected from the set of configured archive directories when more than one
28+
/// is available.
29+
/// </summary>
30+
public enum ArchiveDirectoryFillMethod
31+
{
32+
/// <summary>
33+
/// Fill each configured write path, in order, advancing to the next only when the current path's drive lacks
34+
/// sufficient free space. This is the default and matches the historical archive write behavior.
35+
/// </summary>
36+
Sequential,
37+
38+
/// <summary>
39+
/// Distribute new files across the configured write paths in round-robin order, skipping any path whose drive
40+
/// lacks sufficient free space.
41+
/// </summary>
42+
RoundRobin
43+
}
44+
}

Source/Libraries/GSF.SortedTreeStore/Snap/Services/Configuration/AdvancedServerDatabaseConfig.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public AdvancedServerDatabaseConfig(string databaseName, string mainPath, bool s
6666
DesiredRemainingSpace = 5L * SI2.Giga;
6767
StagingCount = 3;
6868
DirectoryMethod = ArchiveDirectoryMethod.TopDirectoryOnly;
69+
FillMethod = ArchiveDirectoryFillMethod.Sequential;
6970
DiskFlushInterval = 10000;
7071
CacheFlushInterval = 100;
7172
}
@@ -76,6 +77,12 @@ public AdvancedServerDatabaseConfig(string databaseName, string mainPath, bool s
7677
/// </summary>
7778
public ArchiveDirectoryMethod DirectoryMethod { get; set; }
7879

80+
/// <summary>
81+
/// Gets or sets how a write path is selected from the final write paths when more than one is configured.
82+
/// Defaults to <see cref="ArchiveDirectoryFillMethod.Sequential"/>.
83+
/// </summary>
84+
public ArchiveDirectoryFillMethod FillMethod { get; set; }
85+
7986
/// <summary>
8087
/// The name associated with the database.
8188
/// </summary>
@@ -126,7 +133,7 @@ public string FinalFileExtension
126133
public bool ImportAttachedPathsAtStartup { get; set; }
127134

128135
/// <summary>
129-
/// Gets all of the paths that are known by this historian.
136+
/// Gets all paths that are known by this historian.
130137
/// A path can be a file name or a folder.
131138
/// </summary>
132139
public List<string> ImportPaths { get; }
@@ -160,7 +167,7 @@ public string FinalFileExtension
160167
public int DiskFlushInterval { get; set; }
161168

162169
/// <summary>
163-
/// The number of milliseconds before data is taken from it's cache and put in the
170+
/// The number of milliseconds before data is taken from its cache and put in the
164171
/// memory file.
165172
/// </summary>
166173
/// <remarks>
@@ -238,10 +245,12 @@ private void ToWriteProcessorSettings(WriteProcessorSettings settings)
238245
}
239246
else
240247
{
241-
// Final staging file
248+
// Final staging file - this is the only stage that writes across the configured final paths, so the
249+
// fill method (sequential vs round-robin) applies here
242250
rollover.ArchiveSettings.ConfigureOnDisk(finalPaths, DesiredRemainingSpace,
243251
DirectoryMethod, ArchiveEncodingMethod, DatabaseName.ToNonNullNorEmptyString("stage" + stage).RemoveInvalidFileNameCharacters(),
244252
finalFilePendingExtension, finalFileFinalExtension, FileFlags.GetStage(stage));
253+
rollover.ArchiveSettings.FillMethod = FillMethod;
245254
}
246255

247256
rollover.LogPath = m_mainPath;

Source/Libraries/GSF.SortedTreeStore/Snap/Services/Writer/SimplifiedArchiveInitializer'2.cs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
using System;
2626
using System.IO;
2727
using System.Linq;
28+
using System.Threading;
29+
using GSF.Immutable;
2830
using GSF.IO;
2931
using GSF.Snap.Storage;
3032
using GSF.Snap.Types;
@@ -41,6 +43,7 @@ public class SimplifiedArchiveInitializer<TKey, TValue>
4143
{
4244
private SimplifiedArchiveInitializerSettings m_settings;
4345
private readonly ReaderWriterLockEasy m_lock;
46+
private int m_writePathSeed;
4447

4548
/// <summary>
4649
/// Creates a <see cref="ArchiveInitializer{TKey,TValue}"/>
@@ -74,7 +77,7 @@ public void UpdateSettings(SimplifiedArchiveInitializerSettings settings)
7477

7578
/// <summary>
7679
/// Creates a new <see cref="SortedTreeTable{TKey,TValue}"/> based on the settings passed to this class.
77-
/// Once created, it is up to he caller to make sure that this class is properly disposed of.
80+
/// Once created, it is up to the caller to make sure that this class is properly disposed of.
7881
/// </summary>
7982
/// <param name="startKey">the first key in the archive file</param>
8083
/// <param name="endKey">the last key in the archive file</param>
@@ -100,7 +103,7 @@ public SortedTreeTable<TKey, TValue> CreateArchiveFile(TKey startKey, TKey endKe
100103
private string CreateArchiveName(string path)
101104
{
102105
path = GetPath(path, DateTime.Now);
103-
return Path.Combine(path, m_settings.Prefix.ToLower() + "-" + Guid.NewGuid() + "-" + DateTime.UtcNow.Ticks + m_settings.PendingExtension);
106+
return Path.Combine(path, $"{m_settings.Prefix.ToLower()}-{Guid.NewGuid()}-{DateTime.UtcNow.Ticks}{m_settings.PendingExtension}");
104107
}
105108

106109
/// <summary>
@@ -119,7 +122,7 @@ private string CreateArchiveName(string path, TKey startKey, TKey endKey)
119122
return CreateArchiveName(path);
120123

121124
path = GetPath(path, startDate);
122-
return Path.Combine(path, m_settings.Prefix.ToLower() + "-" + startDate.ToString("yyyy-MM-dd HH.mm.ss.fff") + "_to_" + endDate.ToString("yyyy-MM-dd HH.mm.ss.fff") + "-" + DateTime.UtcNow.Ticks + m_settings.PendingExtension);
125+
return Path.Combine(path, $"{m_settings.Prefix.ToLower()}-{startDate:yyyy-MM-dd HH.mm.ss.fff}_to_{endDate:yyyy-MM-dd HH.mm.ss.fff}-{DateTime.UtcNow.Ticks}{m_settings.PendingExtension}");
123126
}
124127

125128
private string GetPath(string rootPath, DateTime time)
@@ -135,25 +138,45 @@ private string GetPath(string rootPath, DateTime time)
135138
rootPath = Path.Combine(rootPath, time.Year.ToString() + time.Month.ToString("00"));
136139
break;
137140
case ArchiveDirectoryMethod.YearThenMonth:
138-
rootPath = Path.Combine(rootPath, time.Year.ToString() + '\\' + time.Month.ToString("00"));
141+
rootPath = Path.Combine(rootPath, $"{time.Year}\\{time.Month:00}");
139142
break;
140143
}
144+
141145
if (!Directory.Exists(rootPath))
142146
Directory.CreateDirectory(rootPath);
147+
143148
return rootPath;
144149
}
145150

151+
// Selects a write path with enough free space using the configured fill method. Sequential fill always starts
152+
// at the first path (filling each in order); round-robin rotates the starting candidate via a per-instance
153+
// interlocked seed so new files are distributed across the configured write paths.
146154
private string GetPathWithEnoughSpace(long estimatedSize)
147155
{
156+
ImmutableList<string> paths = m_settings.WritePath;
157+
int count = paths.Count;
158+
159+
int start = m_settings.FillMethod == ArchiveDirectoryFillMethod.RoundRobin ?
160+
((Interlocked.Increment(ref m_writePathSeed) - 1) % count + count) % count : 0;
161+
162+
// No size estimate: return the selected path without a free-space check
148163
if (estimatedSize < 0)
149-
return m_settings.WritePath.First();
164+
return paths[start];
165+
150166
long remainingSpace = m_settings.DesiredRemainingSpace;
151-
foreach (string path in m_settings.WritePath)
167+
168+
// Scan all candidate paths once, beginning at the selected position and wrapping around, returning the
169+
// first whose drive has enough free space to store the estimated file while leaving the desired remaining space
170+
for (int offset = 0; offset < count; offset++)
152171
{
172+
string path = paths[(start + offset) % count];
173+
153174
FilePath.GetAvailableFreeSpace(path, out long freeSpace, out _);
175+
154176
if (freeSpace - estimatedSize > remainingSpace)
155177
return path;
156178
}
179+
157180
throw new Exception("Out of free space");
158181
}
159182

Source/Libraries/GSF.SortedTreeStore/Snap/Services/Writer/SimplifiedArchiveInitializerSettings.cs

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public class SimplifiedArchiveInitializerSettings
3838
: SettingsBase<SimplifiedArchiveInitializerSettings>
3939
{
4040
private ArchiveDirectoryMethod m_directoryMethod;
41+
private ArchiveDirectoryFillMethod m_fillMethod;
4142
private string m_prefix;
4243
private string m_pendingExtension;
4344
private string m_finalExtension;
@@ -57,6 +58,7 @@ public SimplifiedArchiveInitializerSettings()
5758
private void Initialize()
5859
{
5960
m_directoryMethod = ArchiveDirectoryMethod.TopDirectoryOnly;
61+
m_fillMethod = ArchiveDirectoryFillMethod.Sequential;
6062
m_prefix = string.Empty;
6163
m_pendingExtension = ".~d2i";
6264
m_finalExtension = ".d2i";
@@ -83,6 +85,20 @@ public ArchiveDirectoryMethod DirectoryMethod
8385
}
8486
}
8587

88+
/// <summary>
89+
/// Gets or sets the method used to select a write path when more than one is configured. Defaults to
90+
/// <see cref="ArchiveDirectoryFillMethod.Sequential"/>.
91+
/// </summary>
92+
public ArchiveDirectoryFillMethod FillMethod
93+
{
94+
get => m_fillMethod;
95+
set
96+
{
97+
TestForEditable();
98+
m_fillMethod = value;
99+
}
100+
}
101+
86102
/// <summary>
87103
/// Gets/Sets the file prefix. Can be String.Empty for no prefix.
88104
/// </summary>
@@ -92,13 +108,16 @@ public string Prefix
92108
set
93109
{
94110
TestForEditable();
111+
95112
if (string.IsNullOrWhiteSpace(value))
96113
{
97114
m_prefix = string.Empty;
98115
return;
99116
}
117+
100118
if (value.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
101-
throw new ArgumentException("filename has invalid characters.", "value");
119+
throw new ArgumentException("filename has invalid characters.", nameof(value));
120+
102121
m_prefix = value;
103122
}
104123
}
@@ -149,9 +168,7 @@ public EncodingDefinition EncodingMethod
149168
set
150169
{
151170
TestForEditable();
152-
if (value is null)
153-
throw new ArgumentNullException("value");
154-
m_encodingMethod = value;
171+
m_encodingMethod = value ?? throw new ArgumentNullException(nameof(value));
155172
}
156173
}
157174

@@ -168,18 +185,13 @@ public long DesiredRemainingSpace
168185
set
169186
{
170187
TestForEditable();
188+
171189
if (value < 100 * 1024L * 1024L)
172-
{
173190
m_desiredRemainingSpace = 100 * 1024L * 1024L;
174-
}
175191
else if (value > 1024 * 1024L * 1024L * 1024L)
176-
{
177192
m_desiredRemainingSpace = 1024 * 1024L * 1024L * 1024L;
178-
}
179193
else
180-
{
181194
m_desiredRemainingSpace = value;
182-
}
183195
}
184196
}
185197

@@ -212,8 +224,9 @@ public void ConfigureOnDisk(IEnumerable<string> paths, long desiredRemainingSpac
212224

213225
public override void Save(Stream stream)
214226
{
215-
stream.Write((byte)1);
227+
stream.Write((byte)2);
216228
stream.Write((int)m_directoryMethod);
229+
stream.Write((int)m_fillMethod);
217230
stream.Write(m_prefix);
218231
stream.Write(m_pendingExtension);
219232
stream.Write(m_finalExtension);
@@ -238,21 +251,30 @@ public override void Load(Stream stream)
238251
switch (version)
239252
{
240253
case 1:
254+
case 2:
241255
m_directoryMethod = (ArchiveDirectoryMethod)stream.ReadInt32();
256+
257+
// Fill method was introduced in version 2; version 1 streams default to Sequential
258+
m_fillMethod = version >= 2 ? (ArchiveDirectoryFillMethod)stream.ReadInt32() : ArchiveDirectoryFillMethod.Sequential;
259+
242260
m_prefix = stream.ReadString();
243261
m_pendingExtension = stream.ReadString();
244262
m_finalExtension = stream.ReadString();
245263
m_desiredRemainingSpace = stream.ReadInt64();
246264
m_encodingMethod = new EncodingDefinition(stream);
265+
247266
int cnt = stream.ReadInt32();
248267
m_writePath.Clear();
268+
249269
while (cnt > 0)
250270
{
251271
cnt--;
252272
m_writePath.Add(stream.ReadString());
253273
}
274+
254275
cnt = stream.ReadInt32();
255276
m_flags.Clear();
277+
256278
while (cnt > 0)
257279
{
258280
cnt--;
@@ -270,6 +292,5 @@ public override void Validate()
270292
if (WritePath.Count == 0)
271293
throw new Exception("Missing write paths.");
272294
}
273-
274295
}
275296
}

Source/Libraries/openHistorian.Core/Net/HistorianServerDatabaseConfig.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ public ArchiveDirectoryMethod DirectoryMethod
6969
set => m_config.DirectoryMethod = value;
7070
}
7171

72+
/// <summary>
73+
/// Specify how a write path is selected from the final archive directories when more than one is configured.
74+
/// </summary>
75+
public ArchiveDirectoryFillMethod FillMethod
76+
{
77+
get => m_config.FillMethod;
78+
set => m_config.FillMethod = value;
79+
}
80+
7281
/// <summary>
7382
/// Gets or sets the desired size of the final stage archive files.
7483
/// </summary>
@@ -126,7 +135,7 @@ public int DiskFlushInterval
126135
}
127136

128137
/// <summary>
129-
/// The number of milliseconds before data is taken from it's cache and put in the
138+
/// The number of milliseconds before data is taken from its cache and put in the
130139
/// memory file.
131140
/// </summary>
132141
/// <remarks>
@@ -181,7 +190,7 @@ public bool ImportAttachedPathsAtStartup
181190
}
182191

183192
/// <summary>
184-
/// Gets all of the paths that are known by this historian.
193+
/// Gets all the paths that are known by this historian.
185194
/// A path can be a file name or a folder.
186195
/// </summary>
187196
public List<string> ImportPaths => m_config.ImportPaths;

Source/openHistorian.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MD/@EntryIndexedValue">MD</s:String>
66
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=URL/@EntryIndexedValue">URL</s:String>
77
<s:Boolean x:Key="/Default/GrammarAndSpelling/GrammarChecking/Exceptions/=primary_0020install_0020location/@EntryIndexedValue">True</s:Boolean>
8+
<s:Boolean x:Key="/Default/GrammarAndSpelling/GrammarChecking/Exceptions/=round_0020robin/@EntryIndexedValue">True</s:Boolean>
89
<s:Boolean x:Key="/Default/UserDictionary/Words/=allusers/@EntryIndexedValue">True</s:Boolean>
910
<s:Boolean x:Key="/Default/UserDictionary/Words/=ALOG/@EntryIndexedValue">True</s:Boolean>
1011
<s:Boolean x:Key="/Default/UserDictionary/Words/=Ampq/@EntryIndexedValue">True</s:Boolean>

0 commit comments

Comments
 (0)