From 751e1a107195f0c9c98c866e8267a5a760545982 Mon Sep 17 00:00:00 2001 From: vnugent Date: Thu, 12 Jan 2023 17:47:41 -0500 Subject: Large project reorder and consolidation --- .../src/Exceptions/MessageTooLargeException.cs | 51 +++++ .../src/Exceptions/SessionStatusException.cs | 46 ++++ .../src/Exceptions/SessionUpdateFailedException.cs | 41 ++++ .../src/GlobalCacheStore.cs | 64 ++++++ .../src/IRemoteCacheStore.cs | 48 ++++ .../src/RemoteSession.cs | 188 ++++++++++++++++ .../src/SessionCacheClient.cs | 242 +++++++++++++++++++++ .../src/VNLib.Plugins.Sessions.Cache.Client.csproj | 44 ++++ 8 files changed, 724 insertions(+) create mode 100644 libs/VNLib.Plugins.Sessions.Cache.Client/src/Exceptions/MessageTooLargeException.cs create mode 100644 libs/VNLib.Plugins.Sessions.Cache.Client/src/Exceptions/SessionStatusException.cs create mode 100644 libs/VNLib.Plugins.Sessions.Cache.Client/src/Exceptions/SessionUpdateFailedException.cs create mode 100644 libs/VNLib.Plugins.Sessions.Cache.Client/src/GlobalCacheStore.cs create mode 100644 libs/VNLib.Plugins.Sessions.Cache.Client/src/IRemoteCacheStore.cs create mode 100644 libs/VNLib.Plugins.Sessions.Cache.Client/src/RemoteSession.cs create mode 100644 libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionCacheClient.cs create mode 100644 libs/VNLib.Plugins.Sessions.Cache.Client/src/VNLib.Plugins.Sessions.Cache.Client.csproj (limited to 'libs/VNLib.Plugins.Sessions.Cache.Client/src') diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/Exceptions/MessageTooLargeException.cs b/libs/VNLib.Plugins.Sessions.Cache.Client/src/Exceptions/MessageTooLargeException.cs new file mode 100644 index 0000000..5a096a6 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.Cache.Client/src/Exceptions/MessageTooLargeException.cs @@ -0,0 +1,51 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Sessions.Cache.Client +* File: MessageTooLargeException.cs +* +* MessageTooLargeException.cs is part of VNLib.Plugins.Sessions.Cache.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Sessions.Cache.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Sessions.Cache.Client is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Runtime.Serialization; + +using VNLib.Net.Messaging.FBM; + +namespace VNLib.Plugins.Sessions.Cache.Client.Exceptions +{ + /// + /// Raised when a request message is too large to send to + /// the server and the server may close the connection. + /// + public class MessageTooLargeException : FBMException + { + /// + public MessageTooLargeException() + { } + /// + public MessageTooLargeException(string message) : base(message) + { } + /// + public MessageTooLargeException(string message, Exception innerException) : base(message, innerException) + { } + /// + protected MessageTooLargeException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/Exceptions/SessionStatusException.cs b/libs/VNLib.Plugins.Sessions.Cache.Client/src/Exceptions/SessionStatusException.cs new file mode 100644 index 0000000..bc6b217 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.Cache.Client/src/Exceptions/SessionStatusException.cs @@ -0,0 +1,46 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Sessions.Cache.Client +* File: SessionStatusException.cs +* +* SessionStatusException.cs is part of VNLib.Plugins.Sessions.Cache.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Sessions.Cache.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Sessions.Cache.Client is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Runtime.Serialization; + +using VNLib.Plugins.Essentials.Sessions; + +namespace VNLib.Plugins.Sessions.Cache.Client.Exceptions +{ + /// + /// Raised when the status of the session is invalid and cannot be used + /// + public class SessionStatusException : SessionException + { + public SessionStatusException() + { } + public SessionStatusException(string message) : base(message) + { } + public SessionStatusException(string message, Exception innerException) : base(message, innerException) + { } + protected SessionStatusException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/Exceptions/SessionUpdateFailedException.cs b/libs/VNLib.Plugins.Sessions.Cache.Client/src/Exceptions/SessionUpdateFailedException.cs new file mode 100644 index 0000000..6d572aa --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.Cache.Client/src/Exceptions/SessionUpdateFailedException.cs @@ -0,0 +1,41 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Sessions.Cache.Client +* File: SessionUpdateFailedException.cs +* +* SessionUpdateFailedException.cs is part of VNLib.Plugins.Sessions.Cache.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Sessions.Cache.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Sessions.Cache.Client is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Runtime.Serialization; + +namespace VNLib.Plugins.Sessions.Cache.Client.Exceptions +{ + public class SessionUpdateFailedException : SessionStatusException + { + public SessionUpdateFailedException() + { } + public SessionUpdateFailedException(string message) : base(message) + { } + public SessionUpdateFailedException(string message, Exception innerException) : base(message, innerException) + { } + protected SessionUpdateFailedException(SerializationInfo info, StreamingContext context) : base(info, context) + { } + } +} diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/GlobalCacheStore.cs b/libs/VNLib.Plugins.Sessions.Cache.Client/src/GlobalCacheStore.cs new file mode 100644 index 0000000..df3c564 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.Cache.Client/src/GlobalCacheStore.cs @@ -0,0 +1,64 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Sessions.Cache.Client +* File: GlobalCacheStore.cs +* +* GlobalCacheStore.cs is part of VNLib.Plugins.Sessions.Cache.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Sessions.Cache.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Sessions.Cache.Client is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Threading; +using System.Threading.Tasks; + +using VNLib.Data.Caching; + +namespace VNLib.Plugins.Sessions.Cache.Client +{ + /// + /// A wrapper class to provide a from + /// a client instance + /// + public sealed class GlobalCacheStore : IRemoteCacheStore + { + private readonly IGlobalCacheProvider _cache; + + public GlobalCacheStore(IGlobalCacheProvider globalCache) + { + _cache = globalCache ?? throw new ArgumentNullException(nameof(globalCache)); + } + + /// + public Task AddOrUpdateObjectAsync(string objectId, string? newId, T obj, CancellationToken cancellationToken = default) + { + return _cache.AddOrUpdateAsync(objectId, newId, obj, cancellationToken); + } + + /// + public Task DeleteObjectAsync(string objectId, CancellationToken cancellationToken = default) + { + return _cache.DeleteAsync(objectId, cancellationToken); + } + + /// + public Task GetObjectAsync(string objectId, CancellationToken cancellationToken = default) + { + return _cache.GetAsync(objectId, cancellationToken); + } + } +} diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/IRemoteCacheStore.cs b/libs/VNLib.Plugins.Sessions.Cache.Client/src/IRemoteCacheStore.cs new file mode 100644 index 0000000..2a8bd49 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.Cache.Client/src/IRemoteCacheStore.cs @@ -0,0 +1,48 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Sessions.Cache.Client +* File: IRemoteCacheStore.cs +* +* IRemoteCacheStore.cs is part of VNLib.Plugins.Sessions.Cache.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Sessions.Cache.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Sessions.Cache.Client is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System.Threading; +using System.Threading.Tasks; + +namespace VNLib.Plugins.Sessions.Cache.Client +{ + /// + /// Represents an asynchronous interface to a remote cache store + /// + public interface IRemoteCacheStore + { + /// + /// Gets an object from the cache provider by key + /// + /// The data type + /// The key/id of the object to recover + /// A token to cancel the operation + /// A task that resolves the found object or null otherwise + Task GetObjectAsync(string objectId, CancellationToken cancellationToken = default); + + Task AddOrUpdateObjectAsync(string objectId, string? newId, T obj, CancellationToken cancellationToken = default); + + Task DeleteObjectAsync(string objectId, CancellationToken cancellationToken = default); + } +} diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/RemoteSession.cs b/libs/VNLib.Plugins.Sessions.Cache.Client/src/RemoteSession.cs new file mode 100644 index 0000000..af2c969 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.Cache.Client/src/RemoteSession.cs @@ -0,0 +1,188 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Sessions.Cache.Client +* File: RemoteSession.cs +* +* RemoteSession.cs is part of VNLib.Plugins.Sessions.Cache.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Sessions.Cache.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Sessions.Cache.Client is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using Microsoft.VisualStudio.Threading; + +using VNLib.Net.Http; +using VNLib.Data.Caching.Exceptions; +using VNLib.Utils.Extensions; +using VNLib.Plugins.Essentials.Sessions; +using VNLib.Plugins.Essentials.Extensions; + +namespace VNLib.Plugins.Sessions.Cache.Client +{ + /// + /// Base class for cacheable lazy initialized session entires + /// that exist in a remote caching server + /// + public abstract class RemoteSession : SessionBase + { + protected const string CREATED_TIME_ENTRY = "__.i.ctime"; + + protected IRemoteCacheStore Client { get; } + protected TimeSpan UpdateTimeout { get; } + + private readonly AsyncLazyInitializer Initializer; + + /// + /// The lazy loaded data-store + /// + protected Dictionary? DataStore; + + protected RemoteSession(string sessionId, IRemoteCacheStore client, TimeSpan backgroundTimeOut) + { + SessionID = sessionId; + UpdateTimeout = backgroundTimeOut; + Client = client; + Initializer = new(InitializeAsync, null); + } + + /// + /// The data initializer, loads the data store from the connected cache server + /// + /// A task that completes when the get operation completes + protected virtual async Task InitializeAsync() + { + //Setup timeout cancellation for the get, to cancel it + using CancellationTokenSource cts = new(UpdateTimeout); + //get or create a new session + DataStore = await Client.GetObjectAsync>(SessionID, cancellationToken: cts.Token); + } + /// + /// Updates the current sessin agaisnt the cache store + /// + /// A task that complets when the update has completed + protected virtual async Task ProcessUpdateAsync() + { + //Setup timeout cancellation for the update, to cancel it + using CancellationTokenSource cts = new(UpdateTimeout); + await Client.AddOrUpdateObjectAsync(SessionID, null, DataStore, cts.Token); + } + /// + /// Delets the current session in the remote store + /// + /// A task that completes when instance has been deleted + protected virtual async Task ProcessDeleteAsync() + { + //Setup timeout cancellation for the update, to cancel it + using CancellationTokenSource cts = new(UpdateTimeout); + try + { + await Client.DeleteObjectAsync(SessionID, cts.Token); + } + catch (ObjectNotFoundException) + { + //This is fine, if the object does not exist, nothing to invalidate + } + } + + /// + public override DateTimeOffset Created + { + get + { + //Deserialze the base32 ms + long unixMs = this.GetValueType(CREATED_TIME_ENTRY); + //set created time from ms + return DateTimeOffset.FromUnixTimeMilliseconds(unixMs); + } + + protected set => this.SetValueType(CREATED_TIME_ENTRY, value.ToUnixTimeMilliseconds()); + } + + + /// + protected override string IndexerGet(string key) + { + //Get the value at the key or an empty string as a default + return DataStore!.GetValueOrDefault(key, string.Empty); + } + /// + protected override void IndexerSet(string key, string value) + { + //If the value is null, remove the key from the store + if (value == null) + { + //Set modified flag + IsModified |= DataStore!.Remove(key); + } + else + { + //Store the value at the specified key + DataStore![key] = value; + IsModified = true; + } + } + + + /* + * If the data-store is not found it means the session does not + * exist in cache, so its technically not dangerous to reuse, + * so the new mask needs to be set, but the old ID is going + * to be reused + */ + + /// + /// Waits for exclusive access to the session, and initializes + /// session data (loads it from the remote store) + /// + /// The event to attach a session to + /// A token to cancel the operaion + /// + public virtual async Task WaitAndLoadAsync(IHttpEvent entity, CancellationToken cancellationToken) + { + //Wait for exclusive access + await base.WaitOneAsync(cancellationToken); + try + { + //Lazily initalize the current instance + await Initializer.InitializeAsync(cancellationToken); + //See if data-store is null (new session was created + if (DataStore == null) + { + //New session was created + DataStore = new(10); + //Set is-new flag + Flags.Set(IS_NEW_MSK); + //Set created time + Created = DateTimeOffset.UtcNow; + //Init ipaddress + UserIP = entity.Server.GetTrustedIp(); + //Set modified flag so session will be updated + IsModified = true; + } + } + catch + { + MainLock.Release(); + throw; + } + } + } +} diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionCacheClient.cs b/libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionCacheClient.cs new file mode 100644 index 0000000..800ad66 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.Cache.Client/src/SessionCacheClient.cs @@ -0,0 +1,242 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Sessions.Cache.Client +* File: SessionCacheClient.cs +* +* SessionCacheClient.cs is part of VNLib.Plugins.Sessions.Cache.Client which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Sessions.Cache.Client is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Sessions.Cache.Client is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +using VNLib.Net.Http; +using VNLib.Utils.Async; +using VNLib.Utils.Logging; +using VNLib.Utils.Memory.Caching; +using VNLib.Plugins.Essentials.Sessions; + +namespace VNLib.Plugins.Sessions.Cache.Client +{ + + /// + /// A client that allows access to sessions located on external servers + /// + public abstract class SessionCacheClient : ICacheHolder + { + public class LRUSessionStore : LRUCache, ICacheHolder where T : ISession + { + internal AsyncQueue ExpiredSessions { get; } + + /// + public override bool IsReadOnly => false; + /// + protected override int MaxCapacity { get; } + + + public LRUSessionStore(int maxCapacity) : base(StringComparer.Ordinal) + { + MaxCapacity = maxCapacity; + ExpiredSessions = new (true, true); + } + + /// + protected override bool CacheMiss(string key, [NotNullWhen(true)] out T? value) + { + value = default; + return false; + } + + /// + protected override void Evicted(KeyValuePair evicted) + { + //add to queue, the list lock should be held during this operatio + _ = ExpiredSessions.TryEnque(evicted.Value); + } + + /// + public void CacheClear() + { + foreach (KeyValuePair value in List) + { + Evicted(value); + } + Clear(); + } + + /// + public void CacheHardClear() + { + CacheClear(); + } + } + + protected readonly LRUSessionStore CacheTable; + protected readonly object CacheLock; + protected readonly int MaxLoadedEntires; + + /// + /// The client used to communicate with the cache server + /// + protected IRemoteCacheStore Store { get; } + + /// + /// Initializes a new + /// + /// + /// The maximum number of sessions to keep in memory + protected SessionCacheClient(IRemoteCacheStore client, int maxCacheItems) + { + MaxLoadedEntires = maxCacheItems; + CacheLock = new(); + CacheTable = new(maxCacheItems); + Store = client; + } + + private ulong _waitingCount; + + /// + /// The number of pending connections waiting for results from the cache server + /// + public ulong WaitingConnections => _waitingCount; + + /// + /// Attempts to get a session from the cache identified by its sessionId asynchronously + /// + /// The connection/request to attach the session to + /// The ID of the session to retrieve + /// A token to cancel the operation + /// A that resolves the remote session + /// + public virtual async ValueTask GetSessionAsync(IHttpEvent entity, string sessionId, CancellationToken cancellationToken) + { + try + { + RemoteSession? session; + //Aquire lock on cache + lock (CacheLock) + { + //See if session is loaded into cache + if (!CacheTable.TryGetValue(sessionId, out session)) + { + //Init new record + session = SessionCtor(sessionId); + //Add to cache + CacheTable.Add(session.SessionID, session); + } + //Valid entry found in cache + } + + //Inc waiting count + Interlocked.Increment(ref _waitingCount); + + try + { + //Load session-data + await session.WaitAndLoadAsync(entity, cancellationToken); + return session; + } + catch + { + //Remove the invalid cached session + lock (CacheLock) + { + _ = CacheTable.Remove(sessionId); + } + throw; + } + finally + { + //Dec waiting count + Interlocked.Decrement(ref _waitingCount); + } + } + catch (SessionException) + { + throw; + } + catch (OperationCanceledException) + { + throw; + } + //Wrap exceptions + catch (Exception ex) + { + throw new SessionException("An unhandled exception was raised", ex); + } + } + + /// + /// Gets a new instances for the given sessionId, + /// and places it a the head of internal cache + /// + /// The session identifier + /// The new session for the given ID + protected abstract RemoteSession SessionCtor(string sessionId); + + /// + /// Begins waiting for expired sessions to be evicted from the cache table that + /// may have pending synchronization operations + /// + /// + /// + /// + public async Task CleanupExpiredSessionsAsync(ILogProvider log, CancellationToken token) + { + while (true) + { + try + { + //Wait for expired session and dispose it + using RemoteSession session = await CacheTable.ExpiredSessions.DequeueAsync(token); + + //Obtain lock on session + await session.WaitOneAsync(CancellationToken.None); + + log.Verbose("Removed expired session {id}", session.SessionID); + } + catch (OperationCanceledException) + { + break; + } + catch(Exception ex) + { + log.Error(ex); + } + } + } + + /// + public void CacheClear() + { + + } + /// + public void CacheHardClear() + { + //Cleanup cache when disconnected + lock (CacheLock) + { + CacheTable.CacheHardClear(); + } + } + } +} diff --git a/libs/VNLib.Plugins.Sessions.Cache.Client/src/VNLib.Plugins.Sessions.Cache.Client.csproj b/libs/VNLib.Plugins.Sessions.Cache.Client/src/VNLib.Plugins.Sessions.Cache.Client.csproj new file mode 100644 index 0000000..f41f9f8 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.Cache.Client/src/VNLib.Plugins.Sessions.Cache.Client.csproj @@ -0,0 +1,44 @@ + + + + net6.0 + Vaughn Nugent + Copyright © 2023 Vaughn Nugent + 1.0.1.1 + True + \\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk + + + + + True + https://www.vaughnnugent.com/resources + latest-all + enable + + + + False + + + + False + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + -- cgit