private async Task RetryEvents(QueueEntryContext <EventPost> context, List <PersistentEvent> eventsToRetry, EventPostInfo ep, IQueueEntry <EventPost> queueEntry) { await _metricsClient.GaugeAsync(MetricNames.EventsRetryCount, eventsToRetry.Count).AnyContext(); foreach (var ev in eventsToRetry) { try { string contentEncoding = null; byte[] data = ev.GetBytes(_jsonSerializerSettings); if (data.Length > 1000) { data = await data.CompressAsync().AnyContext(); contentEncoding = "gzip"; } // Put this single event back into the queue so we can retry it separately. await _queue.Value.EnqueueAsync(new EventPostInfo { ApiVersion = ep.ApiVersion, CharSet = ep.CharSet, ContentEncoding = contentEncoding, Data = data, IpAddress = ep.IpAddress, MediaType = ep.MediaType, ProjectId = ep.ProjectId, UserAgent = ep.UserAgent }, _storage, false, context.CancellationToken).AnyContext(); } catch (Exception ex) { _logger.Error() .Exception(ex) .Critical() .Message("Error while requeuing event post \"{0}\": {1}", queueEntry.Value.FilePath, ex.Message) .Property("Event", new { ev.Date, ev.StackId, ev.Type, ev.Source, ev.Message, ev.Value, ev.Geo, ev.ReferenceId, ev.Tags }) .Project(ep.ProjectId) .Write(); await _metricsClient.CounterAsync(MetricNames.EventsRetryErrors).AnyContext(); } } }
public Task <EventContext> RunAsync(PersistentEvent ev, Organization organization, Project project, EventPostInfo epi = null) { return(RunAsync(new EventContext(ev, organization, project, epi))); }
public Task <ICollection <EventContext> > RunAsync(IEnumerable <PersistentEvent> events, Organization organization, Project project, EventPostInfo epi = null) { return(RunAsync(events.Select(ev => new EventContext(ev, organization, project, epi)).ToList())); }
public EventContext(PersistentEvent ev, Organization organization, Project project, EventPostInfo epi = null) { Organization = organization; Project = project; Event = ev; Event.OrganizationId = organization.Id; Event.ProjectId = project.Id; EventPostInfo = epi; StackSignatureData = new Dictionary <string, string>(); }
protected override async Task <JobResult> ProcessQueueEntryAsync(QueueEntryContext <EventPost> context) { var queueEntry = context.QueueEntry; FileSpec fileInfo = null; await _metricsClient.TimeAsync(async() => fileInfo = await _storage.GetFileInfoAsync(queueEntry.Value.FilePath).AnyContext(), MetricNames.PostsFileInfoTime).AnyContext(); if (fileInfo == null) { await _metricsClient.TimeAsync(() => queueEntry.AbandonAsync(), MetricNames.PostsAbandonTime).AnyContext(); return(JobResult.FailedWithMessage($"Unable to retrieve post data info '{queueEntry.Value.FilePath}'.")); } await _metricsClient.GaugeAsync(MetricNames.PostsMessageSize, fileInfo.Size).AnyContext(); if (fileInfo.Size > GetMaximumEventPostFileSize()) { await _metricsClient.TimeAsync(() => queueEntry.CompleteAsync(), MetricNames.PostsCompleteTime).AnyContext(); return(JobResult.FailedWithMessage($"Unable to process post data '{queueEntry.Value.FilePath}' ({fileInfo.Size} bytes): Maximum event post size limit ({Settings.Current.MaximumEventPostSize} bytes) reached.")); } EventPostInfo ep = null; await _metricsClient.TimeAsync(async() => ep = await _storage.GetEventPostAsync(queueEntry.Value.FilePath, _logger, context.CancellationToken).AnyContext(), MetricNames.PostsMarkFileActiveTime).AnyContext(); if (ep == null) { await AbandonEntryAsync(queueEntry).AnyContext(); return(JobResult.FailedWithMessage($"Unable to retrieve post data '{queueEntry.Value.FilePath}'.")); } await _metricsClient.GaugeAsync(MetricNames.PostsCompressedSize, ep.Data.Length).AnyContext(); bool isInternalProject = ep.ProjectId == Settings.Current.InternalProjectId; _logger.Info() .Message("Processing post: id={0} path={1} project={2} ip={3} v={4} agent={5}", queueEntry.Id, queueEntry.Value.FilePath, ep.ProjectId, ep.IpAddress, ep.ApiVersion, ep.UserAgent) .Property("Id", queueEntry.Id) .Property("ApiVersion", ep.ApiVersion) .Property("IpAddress", ep.IpAddress) .Property("Client", ep.UserAgent) .Tag("processing", "compressed", ep.ContentEncoding) .Value(ep.Data.Length) .Project(ep.ProjectId) .WriteIf(!isInternalProject); var project = await _projectRepository.GetByIdAsync(ep.ProjectId, o => o.Cache()).AnyContext(); if (project == null) { _logger.Error().Message("Unable to process EventPost \"{0}\": Unable to load project: {1}", queueEntry.Value.FilePath, ep.ProjectId).Property("Id", queueEntry.Id).Project(ep.ProjectId).WriteIf(!isInternalProject); await CompleteEntryAsync(queueEntry, ep, SystemClock.UtcNow).AnyContext(); return(JobResult.Success); } long maxEventPostSize = Settings.Current.MaximumEventPostSize; byte[] uncompressedData = ep.Data; if (!String.IsNullOrEmpty(ep.ContentEncoding)) { _logger.Debug().Message("Decompressing EventPost: {0} ({1} bytes)", queueEntry.Id, ep.Data.Length).Property("Id", queueEntry.Id).Tag("decompressing", ep.ContentEncoding).Project(ep.ProjectId).WriteIf(!isInternalProject); maxEventPostSize = GetMaximumUncompressedEventPostSize(); try { await _metricsClient.TimeAsync(async() => { uncompressedData = await uncompressedData.DecompressAsync(ep.ContentEncoding).AnyContext(); }, MetricNames.PostsDecompressionTime).AnyContext(); } catch (Exception ex) { await _metricsClient.CounterAsync(MetricNames.PostsDecompressionErrors).AnyContext(); await CompleteEntryAsync(queueEntry, ep, SystemClock.UtcNow).AnyContext(); return(JobResult.FailedWithMessage($"Unable to decompress EventPost data '{queueEntry.Value.FilePath}' ({ep.Data.Length} bytes compressed): {ex.Message}")); } } await _metricsClient.GaugeAsync(MetricNames.PostsUncompressedSize, fileInfo.Size).AnyContext(); if (uncompressedData.Length > maxEventPostSize) { await CompleteEntryAsync(queueEntry, ep, SystemClock.UtcNow).AnyContext(); return(JobResult.FailedWithMessage($"Unable to process decompressed EventPost data '{queueEntry.Value.FilePath}' ({ep.Data.Length} bytes compressed, {uncompressedData.Length} bytes): Maximum uncompressed event post size limit ({maxEventPostSize} bytes) reached.")); } _logger.Debug().Message("Processing uncompressed EventPost: {0} ({1} bytes)", queueEntry.Id, uncompressedData.Length).Property("Id", queueEntry.Id).Tag("uncompressed").Value(uncompressedData.Length).Project(ep.ProjectId).WriteIf(!isInternalProject); var createdUtc = SystemClock.UtcNow; var events = await ParseEventPostAsync(ep, createdUtc, uncompressedData, queueEntry.Id, isInternalProject).AnyContext(); if (events == null || events.Count == 0) { await CompleteEntryAsync(queueEntry, ep, createdUtc).AnyContext(); return(JobResult.Success); } if (context.CancellationToken.IsCancellationRequested) { await AbandonEntryAsync(queueEntry).AnyContext(); return(JobResult.Cancelled); } bool isSingleEvent = events.Count == 1; if (!isSingleEvent) { await _metricsClient.TimeAsync(async() => { // Don't process all the events if it will put the account over its limits. int eventsToProcess = await _organizationRepository.GetRemainingEventLimitAsync(project.OrganizationId).AnyContext(); // Add 1 because we already counted 1 against their limit when we received the event post. if (eventsToProcess < Int32.MaxValue) { eventsToProcess += 1; } // Discard any events over there limit. events = events.Take(eventsToProcess).ToList(); // Increment the count if greater than 1, since we already incremented it by 1 in the OverageHandler. if (events.Count > 1) { await _organizationRepository.IncrementUsageAsync(project.OrganizationId, false, events.Count - 1, applyHourlyLimit: false).AnyContext(); } }, MetricNames.PostsUpdateEventLimitTime).AnyContext(); } int errorCount = 0; var eventsToRetry = new List <PersistentEvent>(); try { var contexts = await _eventPipeline.RunAsync(events, ep).AnyContext(); _logger.Debug().Message(() => $"Ran {contexts.Count} events through the pipeline: id={queueEntry.Id} success={contexts.Count(r => r.IsProcessed)} error={contexts.Count(r => r.HasError)}").Property("Id", queueEntry.Id).Value(contexts.Count).Project(ep.ProjectId).WriteIf(!isInternalProject); foreach (var ctx in contexts) { if (ctx.IsCancelled) { continue; } if (!ctx.HasError) { continue; } _logger.Error().Exception(ctx.Exception).Message("Error processing EventPost \"{0}\": {1}", queueEntry.Value.FilePath, ctx.ErrorMessage).Property("Id", queueEntry.Id).Project(ep.ProjectId).WriteIf(!isInternalProject); if (ctx.Exception is ValidationException) { continue; } errorCount++; if (!isSingleEvent) { // Put this single event back into the queue so we can retry it separately. eventsToRetry.Add(ctx.Event); } } } catch (Exception ex) { _logger.Error().Exception(ex).Message("Error processing EventPost \"{0}\": {1}", queueEntry.Value.FilePath, ex.Message).Property("Id", queueEntry.Id).Project(ep.ProjectId).WriteIf(!isInternalProject); if (ex is ArgumentException || ex is DocumentNotFoundException) { await CompleteEntryAsync(queueEntry, ep, createdUtc).AnyContext(); return(JobResult.Success); } errorCount++; if (!isSingleEvent) { eventsToRetry.AddRange(events); } } if (eventsToRetry.Count > 0) { await _metricsClient.TimeAsync(() => RetryEvents(context, eventsToRetry, ep, queueEntry), MetricNames.PostsRetryTime).AnyContext(); } if (isSingleEvent && errorCount > 0) { await AbandonEntryAsync(queueEntry).AnyContext(); } else { await CompleteEntryAsync(queueEntry, ep, createdUtc).AnyContext(); } return(JobResult.Success); }
protected async override Task <JobResult> RunInternalAsync(CancellationToken token) { QueueEntry <EventPost> queueEntry = null; try { queueEntry = _queue.Dequeue(TimeSpan.FromSeconds(1)); } catch (Exception ex) { if (!(ex is TimeoutException)) { Log.Error().Exception(ex).Message("An error occurred while trying to dequeue the next EventPost: {0}", ex.Message).Write(); return(JobResult.FromException(ex)); } } if (queueEntry == null) { return(JobResult.Success); } if (token.IsCancellationRequested) { queueEntry.Abandon(); return(JobResult.Cancelled); } EventPostInfo eventPostInfo = _storage.GetEventPostAndSetActive(queueEntry.Value.FilePath); if (eventPostInfo == null) { queueEntry.Abandon(); _storage.SetNotActive(queueEntry.Value.FilePath); return(JobResult.FailedWithMessage(String.Format("Unable to retrieve post data '{0}'.", queueEntry.Value.FilePath))); } bool isInternalProject = eventPostInfo.ProjectId == Settings.Current.InternalProjectId; _statsClient.Counter(MetricNames.PostsDequeued); Log.Info().Message("Processing post: id={0} path={1} project={2} ip={3} v={4} agent={5}", queueEntry.Id, queueEntry.Value.FilePath, eventPostInfo.ProjectId, eventPostInfo.IpAddress, eventPostInfo.ApiVersion, eventPostInfo.UserAgent).WriteIf(!isInternalProject); List <PersistentEvent> events = null; try { _statsClient.Time(() => { events = ParseEventPost(eventPostInfo); Log.Info().Message("Parsed {0} events for post: id={1}", events.Count, queueEntry.Id).WriteIf(!isInternalProject); }, MetricNames.PostsParsingTime); _statsClient.Counter(MetricNames.PostsParsed); _statsClient.Gauge(MetricNames.PostsEventCount, events.Count); } catch (Exception ex) { _statsClient.Counter(MetricNames.PostsParseErrors); queueEntry.Abandon(); _storage.SetNotActive(queueEntry.Value.FilePath); Log.Error().Exception(ex).Message("An error occurred while processing the EventPost '{0}': {1}", queueEntry.Id, ex.Message).Write(); return(JobResult.FromException(ex, String.Format("An error occurred while processing the EventPost '{0}': {1}", queueEntry.Id, ex.Message))); } if (token.IsCancellationRequested) { queueEntry.Abandon(); return(JobResult.Cancelled); } if (events == null) { queueEntry.Abandon(); _storage.SetNotActive(queueEntry.Value.FilePath); return(JobResult.Success); } int eventsToProcess = events.Count; bool isSingleEvent = events.Count == 1; if (!isSingleEvent) { var project = _projectRepository.GetById(eventPostInfo.ProjectId, true); // Don't process all the events if it will put the account over its limits. eventsToProcess = _organizationRepository.GetRemainingEventLimit(project.OrganizationId); // Add 1 because we already counted 1 against their limit when we received the event post. if (eventsToProcess < Int32.MaxValue) { eventsToProcess += 1; } // Increment by count - 1 since we already incremented it by 1 in the OverageHandler. _organizationRepository.IncrementUsage(project.OrganizationId, false, events.Count - 1); } if (events == null) { queueEntry.Abandon(); _storage.SetNotActive(queueEntry.Value.FilePath); return(JobResult.Success); } var errorCount = 0; var created = DateTime.UtcNow; try { events.ForEach(e => e.CreatedUtc = created); var results = _eventPipeline.Run(events.Take(eventsToProcess).ToList()); Log.Info().Message("Ran {0} events through the pipeline: id={1} project={2} success={3} error={4}", results.Count, queueEntry.Id, eventPostInfo.ProjectId, results.Count(r => r.IsProcessed), results.Count(r => r.HasError)).WriteIf(!isInternalProject); foreach (var eventContext in results) { if (eventContext.IsCancelled) { continue; } if (!eventContext.HasError) { continue; } Log.Error().Exception(eventContext.Exception).Project(eventPostInfo.ProjectId).Message("Error while processing event post \"{0}\": {1}", queueEntry.Value.FilePath, eventContext.ErrorMessage).Write(); if (eventContext.Exception is ValidationException) { continue; } errorCount++; if (!isSingleEvent) { // Put this single event back into the queue so we can retry it separately. _queue.Enqueue(new EventPostInfo { ApiVersion = eventPostInfo.ApiVersion, CharSet = eventPostInfo.CharSet, Data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(eventContext.Event)), IpAddress = eventPostInfo.IpAddress, MediaType = eventPostInfo.MediaType, ProjectId = eventPostInfo.ProjectId, UserAgent = eventPostInfo.UserAgent }, _storage, false); } } } catch (ArgumentException ex) { Log.Error().Exception(ex).Project(eventPostInfo.ProjectId).Message("Error while processing event post \"{0}\": {1}", queueEntry.Value.FilePath, ex.Message).Write(); queueEntry.Complete(); } catch (Exception ex) { Log.Error().Exception(ex).Project(eventPostInfo.ProjectId).Message("Error while processing event post \"{0}\": {1}", queueEntry.Value.FilePath, ex.Message).Write(); errorCount++; } if (isSingleEvent && errorCount > 0) { queueEntry.Abandon(); _storage.SetNotActive(queueEntry.Value.FilePath); } else { queueEntry.Complete(); if (queueEntry.Value.ShouldArchive) { _storage.CompleteEventPost(queueEntry.Value.FilePath, eventPostInfo.ProjectId, created, queueEntry.Value.ShouldArchive); } else { _storage.DeleteFile(queueEntry.Value.FilePath); _storage.SetNotActive(queueEntry.Value.FilePath); } } return(JobResult.Success); }
private async Task CompleteEntryAsync(IQueueEntry <EventPost> queueEntry, EventPostInfo eventPostInfo, DateTime created) { await queueEntry.CompleteAsync().AnyContext(); await _storage.CompleteEventPostAsync(queueEntry.Value.FilePath, eventPostInfo.ProjectId, created, _logger, queueEntry.Value.ShouldArchive).AnyContext(); }
public static async Task <string> EnqueueAsync(this IQueue <EventPost> queue, EventPostInfo data, IFileStorage storage, bool shouldArchive, CancellationToken cancellationToken = default(CancellationToken)) { string path; if (shouldArchive) { path = $"archive\\{SystemClock.UtcNow:yy\\\\MM\\\\dd\\\\HH}\\{data.ProjectId}\\{Guid.NewGuid():N}.json"; } else { path = $"q\\{Guid.NewGuid():N}.json"; } if (!await storage.SaveObjectAsync(path, data, cancellationToken).AnyContext()) { return(null); } return(await queue.EnqueueAsync(new EventPost { FilePath = path, ShouldArchive = shouldArchive }).AnyContext()); }
protected override async Task <JobResult> ProcessQueueEntryAsync(JobQueueEntryContext <EventPost> context) { var queueEntry = context.QueueEntry; EventPostInfo eventPostInfo = await _storage.GetEventPostAndSetActiveAsync(queueEntry.Value.FilePath, context.CancellationToken).AnyContext(); if (eventPostInfo == null) { await queueEntry.AbandonAsync().AnyContext(); await _storage.SetNotActiveAsync(queueEntry.Value.FilePath).AnyContext(); return(JobResult.FailedWithMessage($"Unable to retrieve post data '{queueEntry.Value.FilePath}'.")); } bool isInternalProject = eventPostInfo.ProjectId == Settings.Current.InternalProjectId; Logger.Info().Message("Processing post: id={0} path={1} project={2} ip={3} v={4} agent={5}", queueEntry.Id, queueEntry.Value.FilePath, eventPostInfo.ProjectId, eventPostInfo.IpAddress, eventPostInfo.ApiVersion, eventPostInfo.UserAgent).WriteIf(!isInternalProject); List <PersistentEvent> events = null; try { _metricsClient.Time(() => { events = ParseEventPost(eventPostInfo); Logger.Info().Message("Parsed {0} events for post: id={1}", events.Count, queueEntry.Id).WriteIf(!isInternalProject); }, MetricNames.PostsParsingTime); await _metricsClient.CounterAsync(MetricNames.PostsParsed).AnyContext(); await _metricsClient.GaugeAsync(MetricNames.PostsEventCount, events.Count).AnyContext(); } catch (Exception ex) { await queueEntry.AbandonAsync().AnyContext(); await _metricsClient.CounterAsync(MetricNames.PostsParseErrors).AnyContext(); await _storage.SetNotActiveAsync(queueEntry.Value.FilePath).AnyContext(); Logger.Error().Exception(ex).Message("An error occurred while processing the EventPost '{0}': {1}", queueEntry.Id, ex.Message).Write(); return(JobResult.FromException(ex, $"An error occurred while processing the EventPost '{queueEntry.Id}': {ex.Message}")); } if (!events.Any() || context.CancellationToken.IsCancellationRequested) { await queueEntry.AbandonAsync().AnyContext(); await _storage.SetNotActiveAsync(queueEntry.Value.FilePath).AnyContext(); return(!events.Any() ? JobResult.Success : JobResult.Cancelled); } int eventsToProcess = events.Count; bool isSingleEvent = events.Count == 1; if (!isSingleEvent) { var project = await _projectRepository.GetByIdAsync(eventPostInfo.ProjectId, true).AnyContext(); if (project == null) { // NOTE: This could archive the data for a project that no longer exists. Logger.Error().Project(eventPostInfo.ProjectId).Message($"Unable to process EventPost \"{queueEntry.Value.FilePath}\": Unable to load project: {eventPostInfo.ProjectId}").Write(); await CompleteEntryAsync(queueEntry, eventPostInfo, DateTime.UtcNow).AnyContext(); return(JobResult.Success); } // Don't process all the events if it will put the account over its limits. eventsToProcess = await _organizationRepository.GetRemainingEventLimitAsync(project.OrganizationId).AnyContext(); // Add 1 because we already counted 1 against their limit when we received the event post. if (eventsToProcess < Int32.MaxValue) { eventsToProcess += 1; } // Increment by count - 1 since we already incremented it by 1 in the OverageHandler. await _organizationRepository.IncrementUsageAsync(project.OrganizationId, false, events.Count - 1).AnyContext(); } var errorCount = 0; var created = DateTime.UtcNow; try { events.ForEach(e => e.CreatedUtc = created); var results = await _eventPipeline.RunAsync(events.Take(eventsToProcess).ToList()).AnyContext(); Logger.Info().Message("Ran {0} events through the pipeline: id={1} project={2} success={3} error={4}", results.Count, queueEntry.Id, eventPostInfo.ProjectId, results.Count(r => r.IsProcessed), results.Count(r => r.HasError)).WriteIf(!isInternalProject); foreach (var eventContext in results) { if (eventContext.IsCancelled) { continue; } if (!eventContext.HasError) { continue; } Logger.Error().Exception(eventContext.Exception).Project(eventPostInfo.ProjectId).Message("Error while processing event post \"{0}\": {1}", queueEntry.Value.FilePath, eventContext.ErrorMessage).Write(); if (eventContext.Exception is ValidationException) { continue; } errorCount++; if (!isSingleEvent) { // Put this single event back into the queue so we can retry it separately. await _queue.EnqueueAsync(new EventPostInfo { ApiVersion = eventPostInfo.ApiVersion, Data = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(eventContext.Event)), IpAddress = eventPostInfo.IpAddress, MediaType = eventPostInfo.MediaType, CharSet = eventPostInfo.CharSet, ProjectId = eventPostInfo.ProjectId, UserAgent = eventPostInfo.UserAgent }, _storage, false, context.CancellationToken).AnyContext(); } } } catch (Exception ex) { Logger.Error().Exception(ex).Project(eventPostInfo.ProjectId).Message("Error while processing event post \"{0}\": {1}", queueEntry.Value.FilePath, ex.Message).Write(); if (ex is ArgumentException || ex is DocumentNotFoundException) { await queueEntry.CompleteAsync().AnyContext(); } else { errorCount++; } } if (isSingleEvent && errorCount > 0) { await queueEntry.AbandonAsync().AnyContext(); await _storage.SetNotActiveAsync(queueEntry.Value.FilePath).AnyContext(); } else { await CompleteEntryAsync(queueEntry, eventPostInfo, created).AnyContext(); } return(JobResult.Success); }
public EventContext(PersistentEvent ev, Organization organization, Project project, EventPostInfo epi = null) { Organization = organization; Project = project; Event = ev; Event.OrganizationId = organization.Id; Event.ProjectId = project.Id; IncludePrivateInformation = project.Configuration.Settings.GetBoolean(SettingsDictionary.KnownKeys.IncludePrivateInformation, true); EventPostInfo = epi; StackSignatureData = new Dictionary <string, string>(); }
public static Task <string> EnqueueAsync(this IQueue <EventPost> queue, EventPostInfo data, IFileStorage storage) { return(EnqueueAsync(queue, data, storage, Settings.Current.EnableArchive)); }
public Task <EventContext> RunAsync(PersistentEvent ev, EventPostInfo epi = null) { return(RunAsync(new EventContext(ev, epi))); }
public static async Task <string> EnqueueAsync(this IQueue <EventPost> queue, EventPostInfo data, IFileStorage storage, bool shouldArchive = true, CancellationToken cancellationToken = default(CancellationToken)) { string path = String.Format("q\\{0}.json", Guid.NewGuid().ToString("N")); if (!await storage.SaveObjectAsync(path, data, cancellationToken)) { return(null); } return(queue.Enqueue(new EventPost { FilePath = path, ShouldArchive = shouldArchive })); }
public static async Task <string> EnqueueAsync(this IQueue <EventPost> queue, EventPostInfo data, IFileStorage storage, bool shouldArchive, CancellationToken cancellationToken) { string path = $"q\\{Guid.NewGuid():N}.json"; if (!await storage.SaveObjectAsync(path, data, cancellationToken).AnyContext()) { return(null); } return(await queue.EnqueueAsync(new EventPost { FilePath = path, ShouldArchive = shouldArchive }).AnyContext()); }
public static async Task <string> EnqueueAsync(this IQueue <EventPost> queue, EventPostInfo data, IFileStorage storage, bool shouldArchive) { return(await EnqueueAsync(queue, data, storage, shouldArchive, default(CancellationToken)).AnyContext()); }
public EventContext(PersistentEvent ev, EventPostInfo epi = null) { Event = ev; EventPostInfo = epi; StackSignatureData = new Dictionary <string, string>(); }