From 17aabf83a5ca49d497d748061e2d2717e9e6d067 Mon Sep 17 00:00:00 2001 From: "Eric Sibly [chullybun]" Date: Sat, 9 Nov 2024 12:36:12 -0800 Subject: [PATCH] v3.28.0 (#130) * v3.28.0 - *Enhancement:* Added extended capabilities to the `InvokeArgs` to allow additional customization. * Fix failing test. --- CHANGELOG.md | 3 + Common.targets | 2 +- src/CoreEx/Abstractions/Internal.cs | 25 ++++ .../Reflection/PropertyExpression.cs | 4 +- src/CoreEx/Invokers/IInvoker.cs | 34 +++++ src/CoreEx/Invokers/InvokeArgs.cs | 137 +++++++++++------- src/CoreEx/Invokers/InvokerBaseT.cs | 19 ++- src/CoreEx/Invokers/InvokerBaseT2.cs | 19 ++- .../RefData/ReferenceDataOrchestrator.cs | 6 +- .../Framework/Invokers/InvokerBaseTest.cs | 2 +- 10 files changed, 187 insertions(+), 64 deletions(-) create mode 100644 src/CoreEx/Abstractions/Internal.cs create mode 100644 src/CoreEx/Invokers/IInvoker.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index c40fc0b4..e9cdc4d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ Represents the **NuGet** versions. +## v3.28.0 +- *Enhancement:* Added extended capabilities to the `InvokeArgs` to allow additional customization. + ## v3.27.3 - *Fixed:* The `ExecutionContext.Messages` were not being returned as intended within the `x-messages` HTTP Response header; enabled within the `ExtendedStatusCodeResult` and `ExtendedContentResult` on success only (status code `>= 200` and `<= 299`). Note these messages are JSON serialized as the underlying `MessageItemCollection` type. - *Fixed:* The `AgentTester` has been updated to return a `HttpResultAssertor` where the operation returns a `HttpResult` to enable further assertions to be made on the `Result` itself. diff --git a/Common.targets b/Common.targets index b5a6b74f..2988c303 100644 --- a/Common.targets +++ b/Common.targets @@ -1,6 +1,6 @@  - 3.27.3 + 3.28.0 preview Avanade Avanade diff --git a/src/CoreEx/Abstractions/Internal.cs b/src/CoreEx/Abstractions/Internal.cs new file mode 100644 index 00000000..a01ae4aa --- /dev/null +++ b/src/CoreEx/Abstractions/Internal.cs @@ -0,0 +1,25 @@ +// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx + +using Microsoft.Extensions.Caching.Memory; + +namespace CoreEx.Abstractions +{ + /// + /// Provides shareable internal capabilities. + /// + /// This is intended for internal usage only; use with caution. + public static class Internal + { + private static IMemoryCache? _fallbackCache; + + /// + /// Gets the CoreEx fallback . + /// + public static IMemoryCache MemoryCache => ExecutionContext.GetService() ?? (_fallbackCache ??= new MemoryCache(new MemoryCacheOptions())); + + /// + /// Represents a cache for internal capabilities. + /// + public interface IInternalCache : IMemoryCache { } + } +} \ No newline at end of file diff --git a/src/CoreEx/Abstractions/Reflection/PropertyExpression.cs b/src/CoreEx/Abstractions/Reflection/PropertyExpression.cs index 69b00657..e7b1182a 100644 --- a/src/CoreEx/Abstractions/Reflection/PropertyExpression.cs +++ b/src/CoreEx/Abstractions/Reflection/PropertyExpression.cs @@ -16,12 +16,10 @@ namespace CoreEx.Abstractions.Reflection /// public static partial class PropertyExpression { - private static IMemoryCache? _fallbackCache; - /// /// Gets the . /// - internal static IMemoryCache Cache => ExecutionContext.GetService() ?? (_fallbackCache ??= new MemoryCache(new MemoryCacheOptions())); + internal static IMemoryCache Cache => ExecutionContext.GetService() ?? Internal.MemoryCache; /// /// Gets or sets the absolute expiration . diff --git a/src/CoreEx/Invokers/IInvoker.cs b/src/CoreEx/Invokers/IInvoker.cs new file mode 100644 index 00000000..126ac5e2 --- /dev/null +++ b/src/CoreEx/Invokers/IInvoker.cs @@ -0,0 +1,34 @@ +// Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx + +using System.Diagnostics; +using System; +using Microsoft.Extensions.Logging; + +namespace CoreEx.Invokers +{ + /// + /// Enables the standardized invoker capabilities. + /// + public interface IInvoker + { + /// + /// Gets the start of an action. + /// + Action? OnActivityStart { get; } + + /// + /// Gets the action. + /// + Action? OnActivityException { get; } + + /// + /// Gets the completion of an action. + /// + Action? OnActivityComplete { get; } + + /// + /// Get the caller information formatter. + /// + Func CallerLoggerFormatter { get; } + } +} \ No newline at end of file diff --git a/src/CoreEx/Invokers/InvokeArgs.cs b/src/CoreEx/Invokers/InvokeArgs.cs index 36e38ae9..449211b4 100644 --- a/src/CoreEx/Invokers/InvokeArgs.cs +++ b/src/CoreEx/Invokers/InvokeArgs.cs @@ -1,7 +1,9 @@ // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx +using CoreEx.Abstractions; using CoreEx.Configuration; using CoreEx.Results; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System; @@ -15,19 +17,20 @@ namespace CoreEx.Invokers /// public struct InvokeArgs { - private static readonly ConcurrentDictionary _invokerOptions = new(); + private static readonly ConcurrentDictionary _activitySources = new(); private const string NullName = "null"; - private const string InvokerType = "invoker.type"; - private const string InvokerOwner = "invoker.owner"; - private const string InvokerMember = "invoker.member"; - private const string InvokerResult = "invoker.result"; - private const string InvokerFailure = "invoker.failure"; - private const string CompleteState = "Complete"; - private const string SuccessState = "Success"; - private const string FailureState = "Failure"; - private const string ExceptionState = "Exception"; + private const string InvokerTypeName = "invoker.type"; + private const string InvokerOwnerName = "invoker.owner"; + private const string InvokerMemberName = "invoker.member"; + private const string InvokerResultName = "invoker.result"; + private const string InvokerFailureName = "invoker.failure"; + private const string CompleteStateText = "Complete"; + private const string SuccessStateText = "Success"; + private const string FailureStateText = "Failure"; + private const string ExceptionStateText = "Exception"; + private Type? _ownerType; private bool _isComplete; /// @@ -54,52 +57,76 @@ private static bool IsLoggingEnabled(Type invokerType) return settings.GetValue($"Invokers:{invokerType.FullName}:LoggingEnabled") ?? settings.GetValue("Invokers:Default:LoggingEnabled") ?? true; } + /// + /// Provides the default implementation. + /// + /// The . + /// The caller information to be included in the log output. + public static string DefaultCallerLogFormatter(InvokeArgs args) => args.OwnerType is null ? args.MemberName ?? NullName : $"{args.OwnerType}->{args.MemberName ?? NullName}"; + + /// + /// Gets or sets the for tracing and logging enablement determination. + /// + /// These are cached to avoid the overhead of repeated configuration lookups and allow for dynamic configuration changes. + public static TimeSpan AbsoluteExpirationTimeSpan { get; set; } = TimeSpan.FromMinutes(5); + /// /// Initializes a new instance of the struct. /// - public InvokeArgs() => Type = typeof(Invoker); + /// This will throw a . + public InvokeArgs() => throw new NotSupportedException($"The {nameof(InvokeArgs)} default constructor is not supported; please use other(s)."); /// /// Initializes a new instance of the struct. /// - /// The invoker used to manage the activity sources. - /// The invoking (owner) to include as part of the . + /// The initiating . + /// The invoking (owner) value. /// The calling member name. /// The optional parent . - /// Creates the tracing by concatenating the invoking () and separated by ' -> '. This is not + /// Creates the tracing by concatenating the invoking () and separated by ' -> '. This is not /// meant to represent the fully-qualified member/method name. - public InvokeArgs(Type invokerType, Type? ownerType, string? memberName, InvokeArgs? invokeArgs) + public InvokeArgs(IInvoker invoker, object? owner, string? memberName, InvokeArgs? invokeArgs) { - Type = invokerType; - OwnerType = ownerType; + Invoker = invoker ?? throw new ArgumentNullException(nameof(invoker)); + InvokerType = invoker.GetType(); + Owner = owner; MemberName = memberName; try { - var options = _invokerOptions.GetOrAdd(invokerType, type => (new ActivitySource(type.FullName ?? NullName), IsTracingEnabled(invokerType), IsLoggingEnabled(invokerType))); - if (options.IsTracingEnabled) + var enabled = Internal.MemoryCache.GetOrCreate<(bool IsTracingEnabled, bool IsLoggingEnabled)>(InvokerType, e => + { + // These are cached to avoid the overhead of repeated configuration lookups and allow for dynamic configuration changes. + var type = (Type)e.Key; + e.SetAbsoluteExpiration(TimeSpan.FromMinutes(5)); + return (IsTracingEnabled(type), IsLoggingEnabled(type)); + }); + + if (enabled.IsTracingEnabled) { - Activity = options.ActivitySource.CreateActivity(ownerType is null ? memberName ?? NullName : $"{ownerType}->{memberName ?? NullName}", ActivityKind.Internal); + var activitySource = _activitySources.GetOrAdd(InvokerType, type => new ActivitySource(type.FullName ?? NullName)); + Activity = activitySource.CreateActivity(OwnerType is null ? memberName ?? NullName : $"{OwnerType}->{memberName ?? NullName}", ActivityKind.Internal); if (Activity is not null) { if (invokeArgs.HasValue && invokeArgs.Value.Activity is not null) Activity.SetParentId(invokeArgs.Value.Activity!.TraceId, invokeArgs.Value.Activity.SpanId, invokeArgs.Value.Activity.ActivityTraceFlags); - Activity.SetTag(InvokerType, options.ActivitySource!.Name); - Activity.SetTag(InvokerOwner, ownerType?.FullName); - Activity.SetTag(InvokerMember, memberName); + Activity.SetTag(InvokerTypeName, activitySource.Name); + Activity.SetTag(InvokerOwnerName, OwnerType?.FullName); + Activity.SetTag(InvokerMemberName, memberName); + Invoker.OnActivityStart?.Invoke(this); Activity.Start(); } } - if (options.IsLoggingEnabled) + if (enabled.IsLoggingEnabled) { Logger = ExecutionContext.GetService>(); if (Logger is null || !Logger.IsEnabled(LogLevel.Debug)) Logger = null; else { - Logger.LogDebug("{InvokerType}: Start {InvokerCaller}.", invokerType.ToString(), FormatCaller()); + Logger.LogDebug("{InvokerType}: Start {InvokerCaller}.", InvokerType.ToString(), Invoker.CallerLoggerFormatter(this)); Stopwatch = Stopwatch.StartNew(); } } @@ -113,14 +140,24 @@ public InvokeArgs(Type invokerType, Type? ownerType, string? memberName, InvokeA } /// - /// Gets the invoker . + /// Gets the initiating . + /// + public IInvoker Invoker { get; } + + /// + /// Gets the . /// - public Type Type { get; } + public Type InvokerType { get; } /// - /// Gets the invoking (owner) + /// Gets the owning invocation value. /// - public Type? OwnerType { get; } + public object? Owner { get; } + + /// + /// Gets the owning invocation + /// + public Type? OwnerType => _ownerType ??= Owner?.GetType(); /// /// Gets the calling member name. @@ -143,11 +180,6 @@ public InvokeArgs(Type invokerType, Type? ownerType, string? memberName, InvokeA /// public Stopwatch? Stopwatch { get; } - /// - /// Formats the caller. - /// - private readonly string FormatCaller() => OwnerType is null ? MemberName ?? NullName : $"{OwnerType}->{MemberName ?? NullName}"; - /// /// Adds the result outcome to the (where started). /// @@ -160,10 +192,11 @@ public TResult TraceResult(TResult result) if (Activity is not null) { var ir = result as IResult; - Activity.SetTag(InvokerResult, ir is null ? CompleteState : (ir.IsSuccess ? SuccessState : FailureState)); + Activity.SetTag(InvokerResultName, ir is null ? CompleteStateText : (ir.IsSuccess ? SuccessStateText : FailureStateText)); if (ir is not null && ir.IsFailure) - Activity.SetTag(InvokerFailure, $"{ir.Error.Message} ({ir.Error.GetType()})"); + Activity.SetTag(InvokerFailureName, $"{ir.Error.Message} ({ir.Error.GetType()})"); + Invoker.OnActivityComplete?.Invoke(this); _isComplete = true; } @@ -172,7 +205,7 @@ public TResult TraceResult(TResult result) Stopwatch!.Stop(); var ir = result as IResult; Logger.LogDebug("{InvokerType}: {InvokerResult} {InvokerCaller}{InvokerFailure} [{Elapsed}ms].", - Type.ToString(), ir is null ? CompleteState : (ir.IsSuccess ? SuccessState : FailureState), FormatCaller(), (ir is not null && ir.IsFailure) ? $" {ir.Error.Message} ({ir.Error.GetType()})" : string.Empty, Stopwatch.Elapsed.TotalMilliseconds); + InvokerType.ToString(), ir is null ? CompleteStateText : (ir.IsSuccess ? SuccessStateText : FailureStateText), Invoker.CallerLoggerFormatter(this), (ir is not null && ir.IsFailure) ? $" {ir.Error.Message} ({ir.Error.GetType()})" : string.Empty, Stopwatch.Elapsed.TotalMilliseconds); _isComplete = true; } @@ -181,22 +214,23 @@ public TResult TraceResult(TResult result) } /// - /// Completes the tracing (where started) recording the with the and capturing the corresponding . + /// Completes the tracing (where started) recording the with the and capturing the corresponding . /// /// The . public void TraceException(Exception ex) { if (Activity is not null && ex is not null) { - Activity.SetTag(InvokerResult, ExceptionState); - Activity.SetTag(InvokerFailure, $"{ex.Message} ({ex.GetType()})"); + Activity.SetTag(InvokerResultName, ExceptionStateText); + Activity.SetTag(InvokerFailureName, $"{ex.Message} ({ex.GetType()})"); + Invoker.OnActivityException?.Invoke(this, ex); _isComplete = true; } if (Logger is not null && ex is not null) { Stopwatch!.Stop(); - Logger.LogDebug("{InvokerType}: {InvokerResult} {InvokerCaller}{InvokerFailure} [{Elapsed}ms].", Type.ToString(), ExceptionState, FormatCaller(), $" {ex.Message} ({ex.GetType()})", Stopwatch.Elapsed.TotalMilliseconds); + Logger.LogDebug("{InvokerType}: {InvokerResult} {InvokerCaller}{InvokerFailure} [{Elapsed}ms].", InvokerType.ToString(), ExceptionStateText, Invoker.CallerLoggerFormatter(this), $" {ex.Message} ({ex.GetType()})", Stopwatch.Elapsed.TotalMilliseconds); _isComplete = true; } } @@ -204,14 +238,17 @@ public void TraceException(Exception ex) /// /// Completes (stops) the tracing (where started). /// - /// Where not previously recorded as complete will set the to . + /// Where not previously recorded as complete will set the to . public readonly void TraceComplete() { if (Activity is not null) { // Where no result then it can only be as a result of an exception. if (!_isComplete) - Activity.SetTag(InvokerResult, ExceptionState); + { + Activity.SetTag(InvokerResultName, ExceptionStateText); + Invoker.OnActivityException?.Invoke(this, new InvalidOperationException("The invocation was not completed successfully.")); + } Activity.Stop(); } @@ -219,28 +256,28 @@ public readonly void TraceComplete() if (Logger is not null && !_isComplete) { Stopwatch!.Stop(); - Logger.LogDebug("{InvokerType}: {InvokerResult} {InvokerCaller} [{Elapsed}ms].", Type.ToString(), ExceptionState, FormatCaller(), Stopwatch.Elapsed.TotalMilliseconds); + Logger.LogDebug("{InvokerType}: {InvokerResult} {InvokerCaller} [{Elapsed}ms].", InvokerType.ToString(), ExceptionStateText, Invoker.CallerLoggerFormatter(this), Stopwatch.Elapsed.TotalMilliseconds); } } /// /// Creates (and started) a new instance for a related invocation. /// - /// The invoker used to manage the activity sources. - /// The invoking (owner) to include as part of the . + /// The invoker used to manage the activity sources. + /// The invoking (owner) value. /// The calling member name. /// The . - public readonly InvokeArgs StartNewRelated(Type invokerType, Type? ownerType, string? memberName) => new(invokerType, ownerType, memberName, this); + public readonly InvokeArgs StartNewRelated(IInvoker invoker, object? owner, string? memberName) => new(invoker, owner, memberName, this); /// /// Releases (disposes) all instances. /// public static void ReleaseAll() { - foreach (var item in _invokerOptions.ToArray()) + foreach (var item in _activitySources.ToArray()) { - if (_invokerOptions.TryRemove(item.Key, out var options)) - options.ActivitySource?.Dispose(); + if (_activitySources.TryRemove(item.Key, out var activitySource)) + activitySource?.Dispose(); } } } diff --git a/src/CoreEx/Invokers/InvokerBaseT.cs b/src/CoreEx/Invokers/InvokerBaseT.cs index 1417a060..93a5930d 100644 --- a/src/CoreEx/Invokers/InvokerBaseT.cs +++ b/src/CoreEx/Invokers/InvokerBaseT.cs @@ -1,6 +1,7 @@ // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx using System; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -13,8 +14,20 @@ namespace CoreEx.Invokers /// The owner (invoking) . /// All public methods result in either the synchronous or asynchronous virtual methods being called to manage the underlying invocation; therefore, where overridding each should /// be overridden with the same logic. Where no result is specified this defaults to 'object?' for the purposes of execution. - public abstract class InvokerBase + public abstract class InvokerBase : IInvoker { + /// + public Action? OnActivityStart { get; protected set; } + + /// + public Action? OnActivityException { get; protected set; } + + /// + public Action? OnActivityComplete { get; protected set; } + + /// + public Func CallerLoggerFormatter { get; protected set; } = InvokeArgs.DefaultCallerLogFormatter; + /// /// Invokes a with a synchronously. /// @@ -41,7 +54,7 @@ public abstract class InvokerBase /// private TResult TraceOnInvoke(TInvoker invoker, Func func, string? memberName) { - var ia = new InvokeArgs(GetType(), invoker?.GetType(), memberName, null); + var ia = new InvokeArgs(this, invoker, memberName, null); try { return ia.TraceResult(OnInvoke(ia, invoker, func)); @@ -62,7 +75,7 @@ private TResult TraceOnInvoke(TInvoker invoker, Func private async Task TraceOnInvokeAsync(TInvoker invoker, Func> func, string? memberName, CancellationToken cancellationToken) { - var ia = new InvokeArgs(GetType(), invoker?.GetType(), memberName, null); + var ia = new InvokeArgs(this, invoker, memberName, null); try { return ia.TraceResult(await OnInvokeAsync(ia, invoker, func, cancellationToken).ConfigureAwait(false)); diff --git a/src/CoreEx/Invokers/InvokerBaseT2.cs b/src/CoreEx/Invokers/InvokerBaseT2.cs index 715e04ed..49c25fb4 100644 --- a/src/CoreEx/Invokers/InvokerBaseT2.cs +++ b/src/CoreEx/Invokers/InvokerBaseT2.cs @@ -1,6 +1,7 @@ // Copyright (c) Avanade. Licensed under the MIT License. See https://github.com/Avanade/CoreEx using System; +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -14,8 +15,20 @@ namespace CoreEx.Invokers /// The arguments . /// All public methods result in either the synchronous or asynchronous virtual methods being called to manage the underlying invocation; therefore, where overridding each should /// be overridden with the same logic. Where no result is specified this defaults to 'object?' for the purposes of execution. - public abstract class InvokerBase + public abstract class InvokerBase : IInvoker { + /// + public Action? OnActivityStart { get; protected set; } + + /// + public Action? OnActivityException { get; protected set; } + + /// + public Action? OnActivityComplete { get; protected set; } + + /// + public Func CallerLoggerFormatter { get; protected set; } = InvokeArgs.DefaultCallerLogFormatter; + /// /// Invokes a with a synchronously. /// @@ -44,7 +57,7 @@ public abstract class InvokerBase /// private TResult TraceOnInvoke(TInvoker invoker, Func func, TArgs? args, string? memberName) { - var ia = new InvokeArgs(GetType(), invoker?.GetType(), memberName, null); + var ia = new InvokeArgs(this, invoker, memberName, null); try { return ia.TraceResult(OnInvoke(ia, invoker, func, args)); @@ -65,7 +78,7 @@ private TResult TraceOnInvoke(TInvoker invoker, Func private async Task TraceOnInvokeAsync(TInvoker invoker, Func> func, TArgs? args, string? memberName, CancellationToken cancellationToken) { - var ia = new InvokeArgs(GetType(), invoker?.GetType(), memberName, null); + var ia = new InvokeArgs(this, invoker, memberName, null); try { return ia.TraceResult(await OnInvokeAsync(ia, invoker, func, args, cancellationToken).ConfigureAwait(false)); diff --git a/src/CoreEx/RefData/ReferenceDataOrchestrator.cs b/src/CoreEx/RefData/ReferenceDataOrchestrator.cs index 671371bc..2f2d4e3f 100644 --- a/src/CoreEx/RefData/ReferenceDataOrchestrator.cs +++ b/src/CoreEx/RefData/ReferenceDataOrchestrator.cs @@ -276,7 +276,7 @@ public ReferenceDataOrchestrator Register() where TProvider : IRefere Logger.LogDebug("Reference data type {RefDataType} cache load start: ServiceProvider.CreateScope and Threading.ExecutionContext.SuppressFlow to support underlying cache data get.", type.FullName); using var ec = ExecutionContext.Current.CreateCopy(); - var rdo = _asyncLocal.Value; + var rdo = this; using var scope = ServiceProvider.CreateScope(); Task task; @@ -301,7 +301,7 @@ public ReferenceDataOrchestrator Register() where TProvider : IRefere /// /// Performs the actual reference data load in a new thread context / scope. /// - private async Task GetByTypeInNewScopeAsync(ReferenceDataOrchestrator? rdo, ExecutionContext executionContext, IServiceScope scope, Type type, Type providerType, InvokeArgs invokeArgs, CancellationToken cancellationToken) + private async Task GetByTypeInNewScopeAsync(ReferenceDataOrchestrator rdo, ExecutionContext executionContext, IServiceScope scope, Type type, Type providerType, InvokeArgs invokeArgs, CancellationToken cancellationToken) { _asyncLocal.Value = rdo; @@ -309,7 +309,7 @@ private async Task GetByTypeInNewScopeAsync(ReferenceD ExecutionContext.SetCurrent(executionContext); // Start related activity as this "work" is occuring on an unrelated different thread (by design to ensure complete separation). - var ria = invokeArgs.StartNewRelated(typeof(ReferenceDataOrchestratorInvoker), typeof(ReferenceDataOrchestrator), nameof(GetByTypeInNewScopeAsync)); + var ria = invokeArgs.StartNewRelated(invokeArgs.Invoker, rdo, nameof(GetByTypeInNewScopeAsync)); try { if (ria.Activity is not null) diff --git a/tests/CoreEx.Test/Framework/Invokers/InvokerBaseTest.cs b/tests/CoreEx.Test/Framework/Invokers/InvokerBaseTest.cs index b1827312..caf956eb 100644 --- a/tests/CoreEx.Test/Framework/Invokers/InvokerBaseTest.cs +++ b/tests/CoreEx.Test/Framework/Invokers/InvokerBaseTest.cs @@ -36,7 +36,7 @@ public void Invoke_AsyncWithResult() public async Task Invoke_AsyncWithResult_Load() { var i = new TestInvoker(); - for (var j = 0; j < 1000; j++) + for (var j = 0; j < 100000; j++) { await i.InvokeAsync(this, async (_, ct) => { await Task.Delay(0, ct); return 88; }); }