修改工单流程页面,订单列表增加工单按钮
This commit is contained in:
1 parent
f9dcae4610
commit
10bb0803d9
44 files changed
+5920
-1291
No files matched your search
@@ -0,0 +1,508 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LaunchDarkly.EventSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides an EventSource client for consuming Server-Sent Events. Additional details on the Server-Sent Events spec
|
||||
/// can be found at https://html.spec.whatwg.org/multipage/server-sent-events.html
|
||||
/// </summary>
|
||||
public class EventSource : IEventSource, IDisposable
|
||||
{
|
||||
#region Private Fields
|
||||
|
||||
private readonly Configuration _configuration;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
private List<string> _eventDataStringBuffer;
|
||||
private MemoryStream _eventDataUtf8ByteBuffer;
|
||||
private string _eventName;
|
||||
private string _lastEventId;
|
||||
private readonly ExponentialBackoffWithDecorrelation _backOff;
|
||||
private readonly TimeSpan _configuredInitialRetryDelay;
|
||||
private readonly TimeSpan _configuredMaxRetryDelay;
|
||||
private CancellationTokenSource _currentRequestToken;
|
||||
private readonly CancellationTokenSource _shutdownTokenSource = new CancellationTokenSource();
|
||||
private static readonly TimeSpan MaxSleepTime = TimeSpan.FromMilliseconds(int.MaxValue);
|
||||
private readonly Stopwatch _connectionTimer = new Stopwatch();
|
||||
private ReadyState _readyState;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Events
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<StateChangedEventArgs> Opened;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<StateChangedEventArgs> Closed;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<MessageReceivedEventArgs> MessageReceived;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<CommentReceivedEventArgs> CommentReceived;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event EventHandler<ExceptionEventArgs> Error;
|
||||
|
||||
#endregion Public Events
|
||||
|
||||
#region Public Properties
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ReadyState ReadyState
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
return _readyState;
|
||||
}
|
||||
}
|
||||
private set
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
_readyState = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal TimeSpan BackOffDelay
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
// Exposed for tests, so that retry-bounds behavior can be asserted directly
|
||||
internal ExponentialBackoffWithDecorrelation BackOff => _backOff;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EventSource" /> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">the configuration</param>
|
||||
public EventSource(Configuration configuration)
|
||||
{
|
||||
_readyState = ReadyState.Raw;
|
||||
|
||||
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||
|
||||
|
||||
_configuredInitialRetryDelay = _configuration.InitialRetryDelay;
|
||||
_configuredMaxRetryDelay = _configuration.MaxRetryDelay;
|
||||
|
||||
_backOff = new ExponentialBackoffWithDecorrelation(_configuredInitialRetryDelay,
|
||||
_configuredMaxRetryDelay);
|
||||
|
||||
_httpClient = _configuration.HttpClient ?? CreateHttpClient();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shortcut for initializing an <see cref="EventSource"/> with only a stream URI
|
||||
/// and no custom properties.
|
||||
/// </summary>
|
||||
/// <param name="uri">the stream URI</param>
|
||||
/// <exception cref="ArgumentNullException">if the URI is null</exception>
|
||||
public EventSource(Uri uri) : this(Configuration.Builder(uri).Build()) {}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task StartAsync()
|
||||
{
|
||||
bool firstTime = true;
|
||||
while (ReadyState != ReadyState.Shutdown)
|
||||
{
|
||||
if (!firstTime)
|
||||
{
|
||||
if (_connectionTimer.IsRunning)
|
||||
{
|
||||
if (_connectionTimer.Elapsed >= _configuration.BackoffResetThreshold)
|
||||
{
|
||||
// Sustained healthy operation reverts any temporary bounds and resets
|
||||
// n. Reverting takes precedence over bounds that were
|
||||
// set during the preceding fault window.
|
||||
ClearTemporaryRetryDelayBounds();
|
||||
_backOff.ResetBackoffN();
|
||||
}
|
||||
_connectionTimer.Reset();
|
||||
}
|
||||
await MaybeWaitWithBackOff();
|
||||
}
|
||||
firstTime = false;
|
||||
|
||||
CancellationTokenSource newRequestTokenSource = null;
|
||||
CancellationToken cancellationToken;
|
||||
lock (this)
|
||||
{
|
||||
if (_readyState == ReadyState.Shutdown)
|
||||
{
|
||||
// in case Close() was called in between the previous ReadyState check and the creation of the new token
|
||||
return;
|
||||
}
|
||||
newRequestTokenSource = new CancellationTokenSource();
|
||||
_currentRequestToken?.Dispose();
|
||||
_currentRequestToken = newRequestTokenSource;
|
||||
}
|
||||
|
||||
if (ReadyState == ReadyState.Connecting || ReadyState == ReadyState.Open)
|
||||
{
|
||||
throw new InvalidOperationException(string.Format(Resources.ErrorAlreadyStarted, ReadyState));
|
||||
}
|
||||
|
||||
SetReadyState(ReadyState.Connecting);
|
||||
cancellationToken = newRequestTokenSource.Token;
|
||||
|
||||
try
|
||||
{
|
||||
await ConnectToEventSourceAsync(cancellationToken);
|
||||
|
||||
// ConnectToEventSourceAsync normally doesn't return, unless it detects that the request has been cancelled.
|
||||
Close(ReadyState.Closed);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
CancelCurrentRequest();
|
||||
|
||||
// If the user called Close(), ReadyState = Shutdown, so errors are irrelevant.
|
||||
if (ReadyState != ReadyState.Shutdown)
|
||||
{
|
||||
Exception realException = e;
|
||||
if (realException is AggregateException ae && ae.InnerExceptions.Count == 1)
|
||||
{
|
||||
realException = ae.InnerException;
|
||||
}
|
||||
if (realException is OperationCanceledException oe)
|
||||
{
|
||||
// This exception could either be the result of us explicitly cancelling a request, in which case we don't
|
||||
// need to do anything else, or it could be that the request timed out.
|
||||
if (oe.CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
realException = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
realException = new TimeoutException();
|
||||
}
|
||||
}
|
||||
|
||||
if (realException != null)
|
||||
{
|
||||
OnError(new ExceptionEventArgs(realException));
|
||||
}
|
||||
Close(ReadyState.Closed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MaybeWaitWithBackOff() {
|
||||
TimeSpan sleepTime = _backOff.GetNextBackOff();
|
||||
if (sleepTime > MaxSleepTime)
|
||||
{
|
||||
// Task.Delay throws for anything above the platform timer ceiling, and this method
|
||||
// runs outside the reconnect loop's exception handling, so an unclamped value
|
||||
// would fault StartAsync and stop the stream permanently.
|
||||
sleepTime = MaxSleepTime;
|
||||
}
|
||||
if (sleepTime > TimeSpan.Zero)
|
||||
{
|
||||
BackOffDelay = sleepTime;
|
||||
try
|
||||
{
|
||||
await Task.Delay(sleepTime, _shutdownTokenSource.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Cancellation happened during the wait, likely intentional close
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Restart(bool resetBackoffDelay)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (_readyState != ReadyState.Open)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (resetBackoffDelay)
|
||||
{
|
||||
_backOff.ResetBackoffN();
|
||||
}
|
||||
}
|
||||
CancelCurrentRequest();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetTemporaryRetryDelayBounds(TimeSpan initialDelay, TimeSpan maxDelay)
|
||||
{
|
||||
_backOff.SetBounds(initialDelay, maxDelay);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ClearTemporaryRetryDelayBounds()
|
||||
{
|
||||
_backOff.SetBounds(_configuredInitialRetryDelay, _configuredMaxRetryDelay);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the connection to the SSE server. The <c>EventSource</c> cannot be reopened after this.
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
if (ReadyState != ReadyState.Shutdown)
|
||||
{
|
||||
Close(ReadyState.Shutdown);
|
||||
}
|
||||
CancelCurrentRequest();
|
||||
_shutdownTokenSource.Cancel();
|
||||
|
||||
// do not dispose httpClient if it is user provided
|
||||
if (_configuration.HttpClient == null)
|
||||
{
|
||||
_httpClient.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent to calling <see cref="Close()"/>.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private HttpClient CreateHttpClient()
|
||||
{
|
||||
var client =_configuration.HttpMessageHandler is null ?
|
||||
new HttpClient() :
|
||||
new HttpClient(_configuration.HttpMessageHandler, false);
|
||||
client.Timeout = _configuration.ResponseStartTimeout;
|
||||
return client;
|
||||
}
|
||||
|
||||
private void CancelCurrentRequest()
|
||||
{
|
||||
CancellationTokenSource requestTokenSource = null;
|
||||
lock (this)
|
||||
{
|
||||
requestTokenSource = _currentRequestToken;
|
||||
_currentRequestToken = null;
|
||||
}
|
||||
if (requestTokenSource != null)
|
||||
{
|
||||
requestTokenSource.Cancel();
|
||||
requestTokenSource.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
internal virtual EventSourceService GetEventSourceService(Configuration configuration)
|
||||
{
|
||||
return new EventSourceService(configuration, _httpClient);
|
||||
}
|
||||
|
||||
private async Task ConnectToEventSourceAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_eventDataStringBuffer = null;
|
||||
_eventDataUtf8ByteBuffer = null;
|
||||
|
||||
var svc = GetEventSourceService(_configuration);
|
||||
|
||||
svc.ConnectionOpened += (o, e) => {
|
||||
_connectionTimer.Restart();
|
||||
SetReadyState(ReadyState.Open, OnOpened, e.Headers);
|
||||
};
|
||||
svc.ConnectionClosed += (o, e) => { SetReadyState(ReadyState.Closed, OnClosed); };
|
||||
|
||||
await svc.GetDataAsync(
|
||||
ProcessResponseLineString,
|
||||
ProcessResponseLineUtf8,
|
||||
_lastEventId,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
private void Close(ReadyState state)
|
||||
{
|
||||
SetReadyState(state, OnClosed);
|
||||
}
|
||||
|
||||
private void ProcessResponseLineString(string content)
|
||||
{
|
||||
if (content == null)
|
||||
{
|
||||
// StreamReader may emit a null if the stream has been closed; there's nothing to
|
||||
// be done at this level in that case
|
||||
return;
|
||||
}
|
||||
if (content.Length == 0)
|
||||
{
|
||||
DispatchEvent();
|
||||
}
|
||||
else
|
||||
{
|
||||
HandleParsedLine(EventParser.ParseLineString(content));
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessResponseLineUtf8(Utf8ByteSpan content)
|
||||
{
|
||||
if (content.Length == 0)
|
||||
{
|
||||
DispatchEvent();
|
||||
}
|
||||
else
|
||||
{
|
||||
HandleParsedLine(EventParser.ParseLineUtf8Bytes(content));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void SetReadyState(ReadyState state, Action<StateChangedEventArgs> action = null,
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>> headers = null)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (_readyState == state || _readyState == ReadyState.Shutdown)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_readyState = state;
|
||||
}
|
||||
|
||||
if (action != null)
|
||||
{
|
||||
action(new StateChangedEventArgs(state, headers));
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleParsedLine(EventParser.Result result)
|
||||
{
|
||||
if (result.IsComment)
|
||||
{
|
||||
OnCommentReceived(new CommentReceivedEventArgs(result.GetValueAsString()));
|
||||
}
|
||||
else if (result.IsDataField)
|
||||
{
|
||||
if (result.ValueString != null)
|
||||
{
|
||||
if (_eventDataStringBuffer is null)
|
||||
{
|
||||
_eventDataStringBuffer = new List<string>(2);
|
||||
}
|
||||
_eventDataStringBuffer.Add(result.ValueString);
|
||||
_eventDataStringBuffer.Add("\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_eventDataUtf8ByteBuffer is null)
|
||||
{
|
||||
_eventDataUtf8ByteBuffer = new MemoryStream(result.ValueBytes.Length + 1);
|
||||
}
|
||||
_eventDataUtf8ByteBuffer.Write(result.ValueBytes.Data, result.ValueBytes.Offset, result.ValueBytes.Length);
|
||||
_eventDataUtf8ByteBuffer.WriteByte((byte)'\n');
|
||||
}
|
||||
}
|
||||
else if (result.IsEventField)
|
||||
{
|
||||
_eventName = result.GetValueAsString();
|
||||
}
|
||||
else if (result.IsIdField)
|
||||
{
|
||||
_lastEventId = result.GetValueAsString();
|
||||
}
|
||||
else if (result.IsRetryField)
|
||||
{
|
||||
if (long.TryParse(result.GetValueAsString(), out var retry))
|
||||
{
|
||||
// Clamp while the value is still an integer.
|
||||
var millis = Math.Max(0,
|
||||
Math.Min(retry, Constants.MaxServerDirectedRetryDelayMillis));
|
||||
_backOff.SetServerDirectedMinDelay(TimeSpan.FromMilliseconds(millis));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DispatchEvent()
|
||||
{
|
||||
var name = _eventName ?? Constants.MessageField;
|
||||
_eventName = null;
|
||||
MessageEvent message;
|
||||
|
||||
if (_eventDataStringBuffer != null)
|
||||
{
|
||||
if (_eventDataStringBuffer.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// remove last item which is always a trailing newline
|
||||
_eventDataStringBuffer.RemoveAt(_eventDataStringBuffer.Count - 1);
|
||||
var dataString = string.Concat(_eventDataStringBuffer);
|
||||
message = new MessageEvent(name, dataString, _lastEventId, _configuration.Uri);
|
||||
|
||||
_eventDataStringBuffer.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_eventDataUtf8ByteBuffer is null || _eventDataUtf8ByteBuffer.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var dataSpan = new Utf8ByteSpan(_eventDataUtf8ByteBuffer.GetBuffer(), 0,
|
||||
(int)_eventDataUtf8ByteBuffer.Length - 1); // remove trailing newline
|
||||
message = new MessageEvent(name, dataSpan, _lastEventId, _configuration.Uri);
|
||||
|
||||
// We've now taken ownership of the original buffer; null out the previous
|
||||
// reference to it so a new one will be created next time
|
||||
_eventDataUtf8ByteBuffer = null;
|
||||
}
|
||||
|
||||
OnMessageReceived(new MessageReceivedEventArgs(message));
|
||||
}
|
||||
|
||||
private void OnOpened(StateChangedEventArgs e) =>
|
||||
Opened?.Invoke(this, e);
|
||||
|
||||
private void OnClosed(StateChangedEventArgs e) =>
|
||||
Closed?.Invoke(this, e);
|
||||
|
||||
private void OnMessageReceived(MessageReceivedEventArgs e) =>
|
||||
MessageReceived?.Invoke(this, e);
|
||||
|
||||
private void OnCommentReceived(CommentReceivedEventArgs e) =>
|
||||
CommentReceived?.Invoke(this, e);
|
||||
|
||||
private void OnError(ExceptionEventArgs e) =>
|
||||
Error?.Invoke(this, e);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user