HeartbeatsBundle.swift 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  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 heartbeats of
  20. /// multiple time periods.
  21. struct HeartbeatsBundle: Codable, HeartbeatsPayloadConvertible {
  22. /// The maximum number of heartbeats that can be stored in the buffer.
  23. let capacity: Int
  24. /// A cache used for keeping track of the last heartbeat date recorded for a given time period.
  25. ///
  26. /// The cache contains the last added date for each time period. The reason only the date is cached is
  27. /// because it's the only piece of information that should be used by clients to determine whether or not
  28. /// to append a new heartbeat.
  29. private(set) var lastAddedHeartbeatDates: [TimePeriod: Date]
  30. /// A ring buffer of heartbeats.
  31. private var buffer: RingBuffer<Heartbeat>
  32. /// A default cache provider that provides a dictionary of all time periods mapping to a default date.
  33. static var cacheProvider: () -> [TimePeriod: Date] {
  34. let timePeriodsAndDates = TimePeriod.allCases.map { ($0, Date.distantPast) }
  35. return { Dictionary(uniqueKeysWithValues: timePeriodsAndDates) }
  36. }
  37. /// Designated initializer.
  38. /// - Parameters:
  39. /// - capacity: The heartbeat capacity of the inititialized collection.
  40. /// - cache: A cache of time periods mapping to dates. Defaults to using static `cacheProvider`.
  41. init(capacity: Int,
  42. cache: [TimePeriod: Date] = cacheProvider()) {
  43. buffer = RingBuffer(capacity: capacity)
  44. self.capacity = capacity
  45. lastAddedHeartbeatDates = cache
  46. }
  47. /// Appends a heartbeat to this collection.
  48. /// - Parameter heartbeat: The heartbeat to append.
  49. mutating func append(_ heartbeat: Heartbeat) {
  50. guard capacity > 0 else {
  51. return // Do not append if capacity is non-positive.
  52. }
  53. do {
  54. // Push the heartbeat to the back of the buffer.
  55. if let overwrittenHeartbeat = try buffer.push(heartbeat) {
  56. // If a heartbeat was overwritten, update the cache to ensure it's date
  57. // is removed.
  58. lastAddedHeartbeatDates = lastAddedHeartbeatDates.mapValues { date in
  59. overwrittenHeartbeat.date == date ? .distantPast : date
  60. }
  61. }
  62. // Update cache with the new heartbeat's date.
  63. heartbeat.timePeriods.forEach {
  64. lastAddedHeartbeatDates[$0] = heartbeat.date
  65. }
  66. } catch let error as RingBuffer<Heartbeat>.Error {
  67. // A ring buffer error occurred while pushing to the buffer so the bundle
  68. // is reset.
  69. self = HeartbeatsBundle(capacity: capacity)
  70. // Create a diagnostic heartbeat to capture the failure and add it to the
  71. // buffer. The failure is added as a key/value pair to the agent string.
  72. // Given that the ring buffer has been reset, it is not expected for the
  73. // second push attempt to fail.
  74. let errorDescription = error.errorDescription.replacingOccurrences(of: " ", with: "-")
  75. let diagnosticHeartbeat = Heartbeat(
  76. agent: "\(heartbeat.agent) error/\(errorDescription)",
  77. date: heartbeat.date,
  78. timePeriods: heartbeat.timePeriods
  79. )
  80. let secondPushAttempt = Result {
  81. try buffer.push(diagnosticHeartbeat)
  82. }
  83. if case .success = secondPushAttempt {
  84. // Update cache with the new heartbeat's date.
  85. diagnosticHeartbeat.timePeriods.forEach {
  86. lastAddedHeartbeatDates[$0] = diagnosticHeartbeat.date
  87. }
  88. }
  89. } catch {
  90. // Ignore other error.
  91. }
  92. }
  93. /// Removes the heartbeat associated with the given date.
  94. /// - Parameter date: The date of the heartbeat needing removal.
  95. /// - Returns: The heartbeat that was removed or `nil` if there was no heartbeat to remove.
  96. @discardableResult
  97. mutating func removeHeartbeat(from date: Date) -> Heartbeat? {
  98. var removedHeartbeat: Heartbeat?
  99. var poppedHeartbeats: [Heartbeat] = []
  100. while let poppedHeartbeat = buffer.pop() {
  101. if poppedHeartbeat.date == date {
  102. removedHeartbeat = poppedHeartbeat
  103. break
  104. }
  105. poppedHeartbeats.append(poppedHeartbeat)
  106. }
  107. poppedHeartbeats.reversed().forEach {
  108. do {
  109. try buffer.push($0)
  110. } catch {
  111. // Ignore error.
  112. }
  113. }
  114. return removedHeartbeat
  115. }
  116. /// Makes and returns a `HeartbeatsPayload` from this heartbeats bundle.
  117. /// - Returns: A heartbeats payload.
  118. func makeHeartbeatsPayload() -> HeartbeatsPayload {
  119. let agentAndDates = buffer.map { heartbeat in
  120. (heartbeat.agent, [heartbeat.date])
  121. }
  122. let userAgentPayloads = [String: [Date]](agentAndDates, uniquingKeysWith: +)
  123. .map(HeartbeatsPayload.UserAgentPayload.init)
  124. .sorted { $0.agent < $1.agent } // Sort payloads by user agent.
  125. return HeartbeatsPayload(userAgentPayloads: userAgentPayloads)
  126. }
  127. }