HeartbeatsBundle.swift 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. // Copyright 2021 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import Foundation
  15. /// A type that can be converted to a `HeartbeatsPayload`.
  16. protocol HeartbeatsPayloadConvertible {
  17. func makeHeartbeatsPayload() -> HeartbeatsPayload
  18. }
  19. /// A codable collection of heartbeats that has a fixed capacity and optimizations for storing
  20. /// heartbeats of
  21. /// multiple time periods.
  22. struct HeartbeatsBundle: Codable, HeartbeatsPayloadConvertible {
  23. /// The maximum number of heartbeats that can be stored in the buffer.
  24. let capacity: Int
  25. /// A cache used for keeping track of the last heartbeat date recorded for a given time period.
  26. ///
  27. /// The cache contains the last added date for each time period. The reason only the date is
  28. /// cached is
  29. /// because it's the only piece of information that should be used by clients to determine whether
  30. /// or not
  31. /// to append a new heartbeat.
  32. private(set) var lastAddedHeartbeatDates: [TimePeriod: Date]
  33. /// A ring buffer of heartbeats.
  34. private var buffer: RingBuffer<Heartbeat>
  35. /// A default cache provider that provides a dictionary of all time periods mapping to a default
  36. /// date.
  37. static var cacheProvider: () -> [TimePeriod: Date] {
  38. let timePeriodsAndDates = TimePeriod.allCases.map { ($0, Date.distantPast) }
  39. return { Dictionary(uniqueKeysWithValues: timePeriodsAndDates) }
  40. }
  41. /// Designated initializer.
  42. /// - Parameters:
  43. /// - capacity: The heartbeat capacity of the inititialized collection.
  44. /// - cache: A cache of time periods mapping to dates. Defaults to using static `cacheProvider`.
  45. init(capacity: Int,
  46. cache: [TimePeriod: Date] = cacheProvider()) {
  47. buffer = RingBuffer(capacity: capacity)
  48. self.capacity = capacity
  49. lastAddedHeartbeatDates = cache
  50. }
  51. /// Appends a heartbeat to this collection.
  52. /// - Parameter heartbeat: The heartbeat to append.
  53. mutating func append(_ heartbeat: Heartbeat) {
  54. guard capacity > 0 else {
  55. return // Do not append if capacity is non-positive.
  56. }
  57. do {
  58. // Push the heartbeat to the back of the buffer.
  59. if let overwrittenHeartbeat = try buffer.push(heartbeat) {
  60. // If a heartbeat was overwritten, update the cache to ensure it's date
  61. // is removed.
  62. lastAddedHeartbeatDates = lastAddedHeartbeatDates.mapValues { date in
  63. overwrittenHeartbeat.date == date ? .distantPast : date
  64. }
  65. }
  66. // Update cache with the new heartbeat's date.
  67. heartbeat.timePeriods.forEach {
  68. lastAddedHeartbeatDates[$0] = heartbeat.date
  69. }
  70. } catch let error as RingBuffer<Heartbeat>.Error {
  71. // A ring buffer error occurred while pushing to the buffer so the bundle
  72. // is reset.
  73. self = HeartbeatsBundle(capacity: capacity)
  74. // Create a diagnostic heartbeat to capture the failure and add it to the
  75. // buffer. The failure is added as a key/value pair to the agent string.
  76. // Given that the ring buffer has been reset, it is not expected for the
  77. // second push attempt to fail.
  78. let errorDescription = error.errorDescription.replacingOccurrences(of: " ", with: "-")
  79. let diagnosticHeartbeat = Heartbeat(
  80. agent: "\(heartbeat.agent) error/\(errorDescription)",
  81. date: heartbeat.date,
  82. timePeriods: heartbeat.timePeriods
  83. )
  84. let secondPushAttempt = Result {
  85. try buffer.push(diagnosticHeartbeat)
  86. }
  87. if case .success = secondPushAttempt {
  88. // Update cache with the new heartbeat's date.
  89. diagnosticHeartbeat.timePeriods.forEach {
  90. lastAddedHeartbeatDates[$0] = diagnosticHeartbeat.date
  91. }
  92. }
  93. } catch {
  94. // Ignore other error.
  95. }
  96. }
  97. /// Removes the heartbeat associated with the given date.
  98. /// - Parameter date: The date of the heartbeat needing removal.
  99. /// - Returns: The heartbeat that was removed or `nil` if there was no heartbeat to remove.
  100. @discardableResult
  101. mutating func removeHeartbeat(from date: Date) -> Heartbeat? {
  102. var removedHeartbeat: Heartbeat?
  103. var poppedHeartbeats: [Heartbeat] = []
  104. while let poppedHeartbeat = buffer.pop() {
  105. if poppedHeartbeat.date == date {
  106. removedHeartbeat = poppedHeartbeat
  107. break
  108. }
  109. poppedHeartbeats.append(poppedHeartbeat)
  110. }
  111. poppedHeartbeats.reversed().forEach {
  112. do {
  113. try buffer.push($0)
  114. } catch {
  115. // Ignore error.
  116. }
  117. }
  118. return removedHeartbeat
  119. }
  120. /// Makes and returns a `HeartbeatsPayload` from this heartbeats bundle.
  121. /// - Returns: A heartbeats payload.
  122. func makeHeartbeatsPayload() -> HeartbeatsPayload {
  123. let agentAndDates = buffer.map { heartbeat in
  124. (heartbeat.agent, [heartbeat.date])
  125. }
  126. let userAgentPayloads = [String: [Date]](agentAndDates, uniquingKeysWith: +)
  127. .map(HeartbeatsPayload.UserAgentPayload.init)
  128. .sorted { $0.agent < $1.agent } // Sort payloads by user agent.
  129. return HeartbeatsPayload(userAgentPayloads: userAgentPayloads)
  130. }
  131. }