jsdw_ios/Pods/CocoaMQTT/Source/CocoaMQTTDeliver.swift

354 lines
11 KiB
Swift

//
// CocoaMQTTDeliver.swift
// CocoaMQTT
//
// Created by HJianBo on 2019/5/2.
// Copyright © 2019 emqx.io. All rights reserved.
//
import Foundation
import Dispatch
protocol CocoaMQTTDeliverProtocol: AnyObject {
var delegateQueue: DispatchQueue { get set }
func deliver(_ deliver: CocoaMQTTDeliver, wantToSend frame: Frame)
}
private struct InflightFrame {
/// The infligth frame maybe a `FramePublish` or `FramePubRel`
var frame: Frame
/// Monotonic time (Dispatch uptime) at which this frame should be retried next.
var nextRetryAtUptimeNs: UInt64
}
extension Array where Element == InflightFrame {
func filterMap(isIncluded: (Element) -> (Bool, Element)) -> [Element] {
var tmp = [Element]()
for e in self {
let res = isIncluded(e)
if res.0 {
tmp.append(res.1)
}
}
return tmp
}
}
// CocoaMQTTDeliver
class CocoaMQTTDeliver: NSObject {
/// The dispatch queue is used by delivering frames in serially
private var deliverQueue = DispatchQueue.init(label: "deliver.cocoamqtt.emqx", qos: .default)
weak var delegate: CocoaMQTTDeliverProtocol?
fileprivate var inflight = [InflightFrame]()
fileprivate var mqueue = [Frame]()
var mqueueSize: UInt = 1000
var inflightWindowSize: UInt = 10
/// Retry time interval millisecond
var retryTimeInterval: Double = 5000
private var awaitingTimer: CocoaMQTTTimer?
var isQueueEmpty: Bool { mqueue.isEmpty }
var isQueueFull: Bool { mqueue.count >= mqueueSize }
var isInflightFull: Bool { inflight.count >= inflightWindowSize }
var isInflightEmpty: Bool { inflight.isEmpty }
var storage: CocoaMQTTStorage?
func recoverSessionBy(_ storage: CocoaMQTTStorage) {
let frames = storage.readAll()
// Sync to push the frame to mqueue for avoiding overcommit
deliverQueue.sync {
self.storage = storage
for f in frames {
mqueue.append(f)
}
if !frames.isEmpty {
printInfo("Deliver recover \(frames.count) msgs")
printDebug("Recover message \(frames)")
}
}
guard !frames.isEmpty else {
return
}
deliverQueue.async { [weak self] in
guard let self = self else { return }
self.tryTransport()
}
}
/// Add a FramePublish to the message queue to wait for sending
///
/// return false means the frame is rejected because of the buffer is full
func add(_ frame: FramePublish) -> Bool {
guard !isQueueFull else {
printError("Sending buffer is full, frame \(frame) has been rejected to add.")
return false
}
// Sync to push the frame to mqueue for avoiding overcommit
deliverQueue.sync {
mqueue.append(frame)
_ = storage?.write(frame)
}
deliverQueue.async { [weak self] in
guard let self = self else { return }
self.tryTransport()
}
return true
}
/// Acknowledge a PUBLISH/PUBREL by msgid
func ack(by frame: Frame) {
let msgid: UInt16
if let puback = frame as? FramePubAck {
msgid = puback.msgid
} else if let pubrec = frame as? FramePubRec {
msgid = pubrec.msgid
} else if let pubcom = frame as? FramePubComp {
msgid = pubcom.msgid
} else {
return
}
let ackType = frame.type
let shouldRemoveFromStorage = frame is FramePubAck || frame is FramePubComp
let ackFrameDescription = String(describing: frame)
deliverQueue.async { [weak self] in
guard let self = self else { return }
let acked = self.ackInflightFrame(withMsgid: msgid, type: ackType)
if acked.count == 0 {
printWarning("Acknowledge by \(ackFrameDescription), but not found in inflight window")
} else {
// TODO: ACK DONT DELETE PUBREL
for f in acked where shouldRemoveFromStorage {
self.storage?.remove(f)
}
printDebug("Acknowledge frame id \(msgid) success, acked: \(acked)")
self.tryTransport()
}
}
}
/// Clean Inflight content to prevent message blocked, when next connection established
///
/// !!Warning: it's a temporary method for hotfix #221
func cleanAll() {
deliverQueue.sync { [weak self] in
guard let self = self else { return }
self.mqueue.removeAll()
self.inflight.removeAll()
}
}
}
// MARK: Private Funcs
extension CocoaMQTTDeliver {
// try transport a frame from mqueue to inflight
private func tryTransport() {
if isQueueEmpty || isInflightFull { return }
// take out the earliest frame
if mqueue.isEmpty { return }
let frame = mqueue.remove(at: 0)
deliver(frame)
// keep trying after a transport
self.tryTransport()
}
/// Try to deliver a frame
private func deliver(_ frame: Frame) {
if frame.qos == .qos0 {
// Send Qos0 message, whatever the in-flight queue is full
// TODO: A retrict deliver mode is need?
sendfun(frame)
} else {
sendfun(frame)
let nowUptimeNs = DispatchTime.now().uptimeNanoseconds
inflight.append(InflightFrame(frame: frame, nextRetryAtUptimeNs: nextRetryDeadline(from: nowUptimeNs)))
// Start a retry timer for resending it if it not receive PUBACK or PUBREC
if awaitingTimer == nil {
awaitingTimer = CocoaMQTTTimer.every(retryTimeInterval / 1000.0, name: "awaitingTimer") { [weak self] in
guard let self = self else { return }
self.deliverQueue.async {
self.redeliver()
}
}
}
}
}
/// Attempt to redeliver in-flight messages
private func redeliver(nowUptimeNs: UInt64 = DispatchTime.now().uptimeNanoseconds) {
if isInflightEmpty {
// Revoke the awaiting timer
awaitingTimer = nil
return
}
for (idx, frame) in inflight.enumerated() where nowUptimeNs >= frame.nextRetryAtUptimeNs {
var duplicatedFrame = frame
duplicatedFrame.frame.dup = true
duplicatedFrame.nextRetryAtUptimeNs = nextRetryDeadline(after: frame.nextRetryAtUptimeNs, nowUptimeNs: nowUptimeNs)
inflight[idx] = duplicatedFrame
printInfo("Re-delivery frame \(duplicatedFrame.frame)")
sendfun(duplicatedFrame.frame)
}
}
private func retryIntervalNanoseconds() -> UInt64 {
let intervalNs = retryTimeInterval * 1_000_000
guard intervalNs.isFinite, intervalNs > 0 else {
return 1
}
if intervalNs >= Double(UInt64.max) {
return UInt64.max
}
return UInt64(intervalNs.rounded())
}
private func nextRetryDeadline(from nowUptimeNs: UInt64) -> UInt64 {
let (nextDeadline, overflow) = nowUptimeNs.addingReportingOverflow(retryIntervalNanoseconds())
return overflow ? UInt64.max : nextDeadline
}
private func nextRetryDeadline(after currentDeadline: UInt64, nowUptimeNs: UInt64) -> UInt64 {
let intervalNs = retryIntervalNanoseconds()
guard nowUptimeNs >= currentDeadline else {
return currentDeadline
}
let missedIntervals = ((nowUptimeNs - currentDeadline) / intervalNs) + 1
let (advance, multiplyOverflow) = intervalNs.multipliedReportingOverflow(by: missedIntervals)
if multiplyOverflow {
return UInt64.max
}
let (nextDeadline, addOverflow) = currentDeadline.addingReportingOverflow(advance)
return addOverflow ? UInt64.max : nextDeadline
}
@discardableResult
private func ackInflightFrame(withMsgid msgid: UInt16, type: FrameType) -> [Frame] {
var ackedFrames = [Frame]()
inflight = inflight.filterMap { frame in
// -- ACK for PUBLISH
if let publish = frame.frame as? FramePublish,
publish.msgid == msgid {
if publish.qos == .qos2 && type == .pubrec { // -- Replace PUBLISH with PUBREL
let pubrel = FramePubRel(msgid: publish.msgid)
var nframe = frame
nframe.frame = pubrel
nframe.nextRetryAtUptimeNs = nextRetryDeadline(from: DispatchTime.now().uptimeNanoseconds)
_ = storage?.write(pubrel)
sendfun(pubrel)
ackedFrames.append(publish)
return (true, nframe)
} else if publish.qos == .qos1 && type == .puback {
ackedFrames.append(publish)
return (false, frame)
}
}
// -- ACK for PUBREL
if let pubrel = frame.frame as? FramePubRel,
pubrel.msgid == msgid && type == .pubcomp {
ackedFrames.append(pubrel)
return (false, frame)
}
return (true, frame)
}
return ackedFrames
}
private func sendfun(_ frame: Frame) {
guard let delegate = self.delegate else {
printError("The deliver delegate is nil!!! the frame will be drop: \(frame)")
return
}
if frame.qos == .qos0 {
if let p = frame as? FramePublish { storage?.remove(p) }
}
delegate.delegateQueue.async {
delegate.deliver(self, wantToSend: frame)
}
}
}
// For tests
extension CocoaMQTTDeliver {
func t_inflightFrames() -> [Frame] {
var frames = [Frame]()
for f in inflight {
frames.append(f.frame)
}
return frames
}
func t_queuedFrames() -> [Frame] {
return mqueue
}
@discardableResult
func t_setInflightNextRetryTime(_ nextRetryAtUptimeNs: UInt64, forMsgid msgid: UInt16) -> Bool {
return deliverQueue.sync {
for idx in inflight.indices {
if let publish = inflight[idx].frame as? FramePublish, publish.msgid == msgid {
inflight[idx].nextRetryAtUptimeNs = nextRetryAtUptimeNs
return true
}
if let pubrel = inflight[idx].frame as? FramePubRel, pubrel.msgid == msgid {
inflight[idx].nextRetryAtUptimeNs = nextRetryAtUptimeNs
return true
}
}
return false
}
}
func t_retryIntervalNanoseconds() -> UInt64 {
return deliverQueue.sync {
retryIntervalNanoseconds()
}
}
func t_redeliver(atUptimeNanoseconds uptimeNs: UInt64) {
deliverQueue.sync {
self.redeliver(nowUptimeNs: uptimeNs)
}
}
}