def deliver_hook(self, instance, payload_override=None): """ Deliver the payload to the target URL. By default it serializes to JSON and POSTs. Args: instance: instance that triggered event. payload_override: JSON-serializable object or callable that will return such object. If callable is used it should accept 2 arguments: `hook` and `instance`. """ if payload_override is None: payload = self.serialize_hook(instance) else: payload = payload_override if callable(payload): payload = payload(self, instance) if getattr(settings, 'HOOK_DELIVERER', None): deliverer = get_module(settings.HOOK_DELIVERER) deliverer(self.target, payload, instance=instance, hook=self) else: client.post( url=self.target, data=json.dumps(payload, cls=DjangoJSONEncoder), headers={'Content-Type': 'application/json'} ) hook_sent_event.send_robust(sender=self.__class__, payload=payload, instance=instance, hook=self) return None
def deliver_hook(self, instance, payload_override=None): """ Deliver the payload to the target URL. By default it serializes to JSON and POSTs. """ payload = payload_override or self.serialize_hook(instance) """ Tweak for when the payload is none no action is taken Needs refactoring: filter hooks by a subset of users """ if payload is None: return None if getattr(settings, 'HOOK_DELIVERER', None): deliverer = get_module(settings.HOOK_DELIVERER) deliverer(self.target, payload, instance=instance, hook=self) else: client.post(url=self.target, data=json.dumps( payload, cls=serializers.json.DjangoJSONEncoder), headers={'Content-Type': 'application/json'}) signals.hook_sent_event.send_robust(sender=self.__class__, payload=payload, instance=instance, hook=self) return None
def deliver_hook(self, instance, payload_override=None): """ Deliver the payload to the target URL. By default it serializes to JSON and POSTs. """ payload = payload_override or self.serialize_hook(instance) """ Tweak for when the payload is none no action is taken Needs refactoring: filter hooks by a subset of users """ if payload is None: return None if getattr(settings, 'HOOK_DELIVERER', None): deliverer = get_module(settings.HOOK_DELIVERER) deliverer(self.target, payload, instance=instance, hook=self) else: client.post( url=self.target, data=json.dumps(payload, cls=serializers.json.DjangoJSONEncoder), headers={'Content-Type': 'application/json'} ) signals.hook_sent_event.send_robust(sender=self.__class__, payload=payload, instance=instance, hook=self) return None
def serialize_hook(self, instance): """ Serialize the object down to Python primitives. By default it uses Django's built in serializer. """ if getattr(instance, 'serialize_hook', None) and callable( instance.serialize_hook): return instance.serialize_hook(hook=self) if getattr(settings, 'HOOK_SERIALIZER', None): serializer = get_module(settings.HOOK_SERIALIZER) return serializer(instance, hook=self) # if no user defined serializers, fallback to the django builtin! data = serializers.serialize('python', [instance])[0] for k, v in data.items(): if isinstance(v, OrderedDict): data[k] = dict(v) if isinstance(data, OrderedDict): data = dict(data) return { 'hook': self.dict(), 'data': data, }
def retry_hook(modeladmin, request, queryset): deliverer = getattr(settings, "HOOK_DELIVERER", None) if not deliverer: modeladmin.message_user(request, "No custom HOOK_DELIVERER set in " "settings.py", messages.ERROR) return deliverer = get_module(deliverer) count = 0 for hook in queryset.filter(target=F("hook__target"), event=F("hook__event"), user_id=F("hook__user_id")): deliverer(hook.target, hook.payload, hook=hook.hook, cleanup=True) count += 1 modeladmin.message_user(request, "Retried %d failed webhooks" % count)
def handle(self, *args, **options): deliverer = getattr(settings, 'HOOK_DELIVERER', None) if not deliverer: raise CommandError("No custom HOOK_DELIVERER set in settings.py") return 5 deliverer = get_module(deliverer) count = 0 for hook in FailedHook.objects.filter(target=F('hook__target'), event=F('hook__event'), user_id=F('hook__user_id')): deliverer(hook.target, hook.payload, hook=hook.hook, cleanup=True) count += 1 self.stdout.write("Retried %d failed webhooks" % count)
def serialize_hook(self, instance): """ Serialize the object down to Python primitives. By default it uses Django's built in serializer. """ if getattr(instance, 'serialize_hook', None) and callable(instance.serialize_hook): return instance.serialize_hook(hook=self) if getattr(settings, 'HOOK_SERIALIZER', None): serializer = get_module(settings.HOOK_SERIALIZER) return serializer(instance, hook=self) # if no user defined serializers, fallback to the django builtin! return { 'hook': self.dict(), 'data': serializers.serialize('python', [instance])[0] }
def handle(self, *args, **options): deliverer = getattr(settings, 'HOOK_DELIVERER', None) if not deliverer: raise CommandError('No custom HOOK_DELIVERER set in settings.py') return 5 deliverer = get_module(deliverer) count = 0 # Backoff schedule: 1min, 3min, 10min, 30min, 60min backoff_minutes = { 1: 1, 2: 2, 3: 7, 4: 20, 5: 30, } for hook in FailedHook.objects.filter(retries__lte=5): # TODO: Add backoff algorithm if hook.last_retry + timedelta(minutes=backoff_minutes.get(hook.retries, 120)) > now(): continue # It's not time yet to retry event_model = hook.event.split('.')[0] + '.' payload_dict = json.loads(hook.payload) has_older_hooks = len(FailedHook.objects.filter( event__startswith=event_model, payload__contains=payload_dict['url'], pk__lt=hook.pk)) > 0 if has_older_hooks: # Don't retry newer hooks if an older one fails for a model url print('Skipping failed hook because there is an older one that ' 'needs to succeed first') continue #TODO: Handle parents and foreign key hooks first deliverer(hook.target, hook.payload, hook=hook.hook, failed_hook=hook) count += 1 self.stdout.write('Retried {} failed webhooks'.format(count))
def deliver_hook(self, instance, payload_override=None): """ Deliver the payload to the target URL. By default it serializes to JSON and POSTs. """ payload = payload_override or self.serialize_hook(instance) if getattr(settings, 'HOOK_DELIVERER', None): deliverer = get_module(settings.HOOK_DELIVERER) deliverer(self.target, payload, instance=instance, hook=self) else: client.post( url=self.target, data=json.dumps(payload, cls=DjangoJSONEncoder), headers={'Content-Type': 'application/json'} ) signals.hook_sent_event.send_robust(sender=self.__class__, payload=payload, instance=instance, hook=self) return None
def deliver_hook(self, instance, payload_override=None): """ Deliver the payload to the target URL. By default it serializes to JSON and POSTs. """ payload = payload_override or self.serialize_hook(instance) if getattr(settings, 'HOOK_DELIVERER', None): deliverer = get_module(settings.HOOK_DELIVERER) deliverer(self.target, payload, instance=instance, hook=self) else: client.post(url=self.target, data=json.dumps(payload, cls=DjangoJSONEncoder), headers={'Content-Type': 'application/json'}) signals.hook_sent_event.send_robust(sender=self.__class__, payload=payload, instance=instance, hook=self) return None
def retry_hook(modeladmin, request, queryset): deliverer = getattr(settings, 'HOOK_DELIVERER', None) if not deliverer: modeladmin.message_user(request, "No custom HOOK_DELIVERER set in " "settings.py", messages.ERROR) return deliverer = get_module(deliverer) count = 0 # Ensure that only "valid" requests are run; TODO: does this really matter? for hook in queryset.filter(target=F('hook__target'), #event=F('hook__event'), # TODO: this won't match for any.any or model.any events user_id=( # For non-superusers, limit access to user's own failed webhooks F('hook__user_id') if request.user.is_superuser else request.user.id) ): deliverer(hook.target, hook.payload, hook=hook.hook, failed_hook=hook) count += 1 modeladmin.message_user(request, "Retried %d failed webhooks" % count)
def sync_flush(self): while len(self.queue) > 0: method, args, kwargs = self.queue.pop() hook_id = kwargs.pop('_hook_id') hook_event = kwargs.pop('_hook_event') hook_user_id = kwargs.pop('_hook_user_id') failed_hook = kwargs.pop('_failed_hook') try: r = None r = getattr(requests, method)(*args, **kwargs) payload = kwargs.get('data', '{}') if r.status_code > 299: if failed_hook: if failed_hook.retries == 5: # send email after several failed attempts, # so that the issue can be investigated r.raise_for_status() failed_hook.response_headers = { k: r.headers[k] for k in r.headers.iterkeys() } failed_hook.response_body = r.content failed_hook.last_status = r.status_code failed_hook.retries = F('retries') + 1 failed_hook.save() else: FailedHook.objects.create( target=r.request.url, payload=payload, response_headers={ k: r.headers[k] for k in r.headers.iterkeys() }, response_body=r.content, last_status=r.status_code, event=hook_event, user_id=hook_user_id, hook_id=hook_id) elif failed_hook: failed_hook.delete() self.total_sent += 1 except (requests.exceptions.HTTPError, requests.exceptions.Timeout, requests.exceptions.RequestException, requests.exceptions.SSLError) as e: send_mail = False # record failed hook for retrying if failed_hook: send_mail = failed_hook.retries == 5 failed_hook.response_headers = { k: r.headers[k] for k in r.headers.iterkeys() } failed_hook.response_body = r.content failed_hook.last_status = r.status_code failed_hook.retries = F('retries') + 1 failed_hook.save() else: send_mail = True #TODO: Consider not sending it on the first go FailedHook.objects.create( target=r.request.url, payload=payload, response_headers={ k: r.headers[k] for k in r.headers.iterkeys() }, response_body=r. content, # TODO: Test what happens when there is no response, e.g. with SSLError last_status=r.status_code, event=hook_event, user_id=hook_user_id, hook_id=hook_id) if send_mail and getattr(settings, 'HOOK_EXCEPTION_MAILER', None): if 'data' in kwargs: kwargs['data'] = json.loads(kwargs['data']) extra_body = {'webhook': kwargs} if r is not None: extra_body['response'] = { 'status_code': r.status_code, 'url': r.url, 'reason': r.reason, 'content': r.content, } mailer = get_module(settings.HOOK_EXCEPTION_MAILER) mailer('Error sending webhook', request=None, exception=e, extra_body=extra_body)