2

I am using PyAPNs to send notifications to iOS devices. I am often sending groups of notifications at once. If any of the tokens is bad for any reason, the process will stop. As a result I am using the enhanced setup and the following method:

apns.gateway_server.register_response_listener

I use this to track which token was the problem and then I pick up from there sending the rest. The issue is that when sending the only way to trap these errors is to use a sleep timer between token sends. For example:

for x in self.retryAPNList:
   apns.gateway_server.send_notification(x, payload, identifier = token)
   time.sleep(0.5)

If I don't use a sleep timer no errors are caught and thus my entire APN list is not sent to as the process stops when there is a bad token. However, this sleep timer is somewhat arbitrary. Sometimes the .5 seconds is enough while other times I have had to set it to 1. In no case has it worked without some sleep delay being added. Doing this slows down web calls and it feels less than bullet proof to enter random sleep times.

Any suggestions for how this can work without a delay between APN calls or is there a best practice for the delay needed?

Adding more code due to the request made below. Here are 3 methods inside of a class that I use to control this:

class PushAdmin(webapp2.RequestHandler):

    retryAPNList=[]
    channelID=""
    channelName = ""
    userName=""

    apns = APNs(use_sandbox=True,cert_file="mycert.pem", key_file="mykey.pem", enhanced=True)

    def devChannelPush(self,channel,name,sendAlerts):
        ucs = UsedChannelStore()
        pus = PushUpdateStore()
        channelName = ""
        refreshApnList = pus.getAPN(channel)
        if sendAlerts:
            alertApnList,channelName = ucs.getAPN(channel)
            if not alertApnList: alertApnList=[]
            if not refreshApnList: refreshApnList=[]
            pushApnList = list(set(alertApnList+refreshApnList))
        elif refreshApnList:
            pushApnList = refreshApnList
        else:
            pushApnList = []

        self.retryAPNList = pushApnList
        self.channelID = channel
        self.channelName = channelName
        self.userName = name
        self.retryAPNPush()

    def retryAPNPush(self):
        token = -1
        payload = Payload(alert="A message from " +self.userName+ " posted to "+self.channelName, sound="default", badge=1, custom={"channel":self.channelID})

        if len(self.retryAPNList)>0:
            token +=1
            for x in self.retryAPNList:
                    self.apns.gateway_server.send_notification(x, payload, identifier = token)
                    time.sleep(0.5)

Below is the calling class (abbreviate to reduce non-related items):

class ChannelStore(ndb.Model):
   def writeMessage(self,ID,name,message,imageKey,fileKey):
        notify = PushAdmin()

        notify.devChannelPush(ID,name,True)

Below is the slight change I made to the placement of the sleep timer that seems to have resolved the issue. I am, however, still concerned for whether the time given will be the right amount in all circumstances.

def retryAPNPush(self):
        identifier = 1
        token = -1
        payload = Payload(alert="A message from " +self.userName+ " posted to "+self.channelName, sound="default", badge=1, custom={"channel":self.channelID})

        if len(self.retryAPNList)>0:
            token +=1
            for x in self.retryAPNList:
                self.apns.gateway_server.send_notification(x, payload, identifier = token)
            time.sleep(0.5)

Resolution:

As noted in the comments at bottom, the resolution to this problem was to move the following statement to the module level outside the class. By doing this there is no need for any sleep statements.

apns = APNs(use_sandbox=True,cert_file="mycert.pem", key_file="mykey.pem", enhanced=True)
C6Silver
  • 3,127
  • 2
  • 21
  • 49
  • this is one of the contributor, may I see more completed code snippet? including how apns is constructed, how retryAPNList is processed, in order to investigate the problem, thanks :) – Jim Horng Mar 22 '15 at 03:32
  • Thanks for your response. I have added quite a bit more code in my OP above. – C6Silver Mar 22 '15 at 05:39

1 Answers1

1

In fact, PyAPNS will auto resend dropped notifications for you, please see PyAPNS

So you don't have to retry by yourself, you can just record what notifications have bad tokens.

The behavior of your code might be result from APNS object kept in local scope (within if len(self.retryAPNList)>0:)

I suggest you to pull out APNS object to class or module level, so that it can complete its error handling procedure and reuse the TCP connection.

Please kindly let me know if it helps, thanks :)

Jim Horng
  • 1,587
  • 1
  • 13
  • 23
  • You are right that I can remove the listener from a re-send perspective. However, the process still does not work without a sleep delay being added. At your suggestion I moved the APNS object to the class level and thus after the if statement you reference I call it with "self.apns". The result is the same without a delay being introduced. I am testing with 2 tokens, the first bad and the second good. With no delay added the second token is never sent as it gets hung up on the first. When I add the delay the second is sent very quickly. – C6Silver Mar 24 '15 at 17:19
  • @C6Silver Can you post log? Thanks :) – Jim Horng Mar 25 '15 at 00:19
  • and whole code including how the class is called? Thanks :) – Jim Horng Mar 25 '15 at 00:39
  • I've re-edited my post above to include the code you requested as well as the call. I do not know what logs to include, however. I am using this on Google's App Engine and the process doesn't throw an error to include here. – C6Silver Mar 25 '15 at 01:55
  • @C6Silver as you can see, ```notify = PushAdmin()``` is still local, why not try this, sleep 10 secs on end of ```ChannelStore.writeMessage()```. You have to keep APNS object in memory after message is sent in order to do further handling. – Jim Horng Mar 25 '15 at 06:23
  • I am not sure what this means. The object is created in PushAdmin and why would the current sleep statement work as is but not without it? That shouldn't have anything to do with the object being in memory. The object is being instantiated once and called in a loop without exiting. – C6Silver Mar 26 '15 at 01:29
  • 1
    @C6Silver I guess the the loop ends quickly without sleep since you only send a few notifications, then PushAdmin() object ends while ChannelStore.writeMessage() finished. You can try making ChannelStore.writeMessage() runs longer to see if bad-token notifications are being handled, or if its not clear enough, we can go back to github so that it's easier to paste code for discussion :) – Jim Horng Mar 26 '15 at 03:07
  • Jim, I took your idea and made a change to the placement of the sleep timer. You can see the new code in my OP at top (I added the new piece to that bottom of that post). Essentially I just moved the sleep timer out of the loop so that it only executes once at the end (I kept the .5 seconds). This does seem to have solved the issue although it leaves me a bit concerned about whether the sleep time will work in all circumstances. It's a bit of a hack but workable. I thank you very much for sticking with me through this! – C6Silver Mar 26 '15 at 04:39
  • Jim, I have discovered a problem related to the solution here. While the sleep timer technically works, the amount of time required to sleep depends on the number of bad tokens. So a magic number of say .5 or 1 doesn't really work if there are many bad tokens. As Apple doesn't tell us when they have processed the entire list of tokens is there a suggestion here? – C6Silver Mar 26 '15 at 16:24
  • For others that may come along, the resolution to this issue is to move the apns = APNs(use_sandbox=True,cert_file="mycert.pem", key_file="mykey.pem", enhanced=True) outside of the class. This resolves the problem without need for any sleep statements. So I again thank Jim for getting me to this success point! – C6Silver Mar 26 '15 at 18:05