summaryrefslogtreecommitdiff
path: root/wocky/wocky-heartbeat-source.c
blob: 73dd3ad47a8a11e690278527282e9ff27ec74541 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
/*
 * wocky-heartbeat-source.c: a GSource wrapping libiphb.
 * Copyright © 2010 Collabora Ltd. <http://www.collabora.co.uk>
 * Copyright © 2010 Nokia Corporation
 * @author Will Thompson <will.thompson@collabora.co.uk>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "wocky-heartbeat-source.h"

#include <errno.h>

#define DEBUG_FLAG DEBUG_HEARTBEAT
#include "wocky-debug.h"

#ifdef HAVE_IPHB
# include <iphbd/libiphb.h>
#endif

typedef struct _WockyHeartbeatSource {
    GSource parent;

#ifdef HAVE_IPHB
    iphb_t heartbeat;
    GPollFD fd;
#endif

    guint max_interval;

    gint64 next_wakeup;
} WockyHeartbeatSource;

#if HAVE_IPHB
static void
wocky_heartbeat_source_degrade (WockyHeartbeatSource *self)
{
  /* If we were using the heartbeat before, stop using it. */
  if (self->heartbeat != NULL)
    {
      GSource *source = (GSource *) self;

      /* If this is being called from wocky_heartbeat_source_finalize(), the
       * source has been destroyed (which implicitly removes all polls.
       */
      if (!g_source_is_destroyed (source))
        g_source_remove_poll (source, &self->fd);

      DEBUG ("closing heartbeat connection");
      iphb_close (self->heartbeat);
      self->heartbeat = NULL;
    }
}

static guint
recommended_intervals[] = {
    IPHB_GS_WAIT_10_HOURS,
    IPHB_GS_WAIT_2_HOURS,
    IPHB_GS_WAIT_1_HOUR,
    IPHB_GS_WAIT_30_MINS,
    IPHB_GS_WAIT_10_MINS * 2, /* It aligns with the 1 hour slot. */
    IPHB_GS_WAIT_10_MINS,
    IPHB_GS_WAIT_5_MINS,
    IPHB_GS_WAIT_2_5_MINS,
    IPHB_GS_WAIT_30_SEC};

static guint
get_system_sync_interval (guint max_interval)
{
  guint i;

  for (i = 0; i < G_N_ELEMENTS (recommended_intervals); i++)
    {
      if (recommended_intervals[i] <= max_interval)
        return recommended_intervals[i];
    }

  return max_interval;
}

static void
wocky_heartbeat_source_wait (
    WockyHeartbeatSource *self,
    guint max_interval)
{
  guint interval;
  int ret;

  if (self->heartbeat == NULL)
    return;

  if (max_interval > 0)
    {
      /* Passing the same minimum and maximum interval to iphb_wait() means
       * that the iphb daemon will wake us up when its internal time is a
       * multiple of the interval.
       * By using recommended intervals across the platform we can get
       * multiple processes waken up at the same time. */
      interval = get_system_sync_interval (max_interval);
      DEBUG ("requested %u as maximum interval; using the recommended %u "
          "interval", max_interval, interval);
      ret = iphb_wait (self->heartbeat, interval, interval, 0);
    }
  else
    {
      ret = iphb_I_woke_up (self->heartbeat);
    }

  if (ret == -1)
    {
      DEBUG ("waiting %u failed: %s; falling back to internal timeouts",
          max_interval, g_strerror (errno));
      wocky_heartbeat_source_degrade (self);
    }
}
#endif

static gboolean
wocky_heartbeat_source_prepare (
    GSource *source,
    gint *msec_to_poll)
{
  WockyHeartbeatSource *self = (WockyHeartbeatSource *) source;
  gint64 now;

#if HAVE_IPHB
  /* If we're listening to the system heartbeat, always rely on it to wake us
   * up.
   */
  if (self->heartbeat != NULL)
    {
      *msec_to_poll = -1;
      return FALSE;
    }
#endif

  if (self->max_interval == 0)
    return FALSE;

  now = g_source_get_time (source);

  /* If now > self->next_wakeup, it's already time to wake up. */
  if (now > self->next_wakeup)
    {
      DEBUG ("ready to wake up (at %li)", now);
      return TRUE;
    }

  /* Otherwise, we should only go back to sleep for a period of
   * (self->next_wakeup - now). Inconveniently, g_source_get_time() gives us µs
   * but we need to return ms; hence the scaling.
   *
   * The value calculated here will always be positive. The difference in
   * seconds is non-negative; if it's zero, the difference in microseconds is
   * positive.
   */
  *msec_to_poll = (self->next_wakeup - now) / 1000;

  return FALSE;
}

static gboolean
wocky_heartbeat_source_check (
    GSource *source)
{
  WockyHeartbeatSource *self = (WockyHeartbeatSource *) source;
  gint64 now;

#ifdef HAVE_IPHB
  if (self->heartbeat != NULL)
    {
      if ((self->fd.revents & (G_IO_ERR | G_IO_HUP)) != 0)
        {
          DEBUG ("Heartbeat closed unexpectedly: %hu; "
              "falling back to internal timeouts", self->fd.revents);
          wocky_heartbeat_source_degrade (self);
          return FALSE;
        }
      else if ((self->fd.revents & G_IO_IN) != 0)
        {
          DEBUG ("Heartbeat fired");
          return TRUE;
        }
      else
        {
          return FALSE;
        }
    }
#endif

  if (self->max_interval == 0)
    return FALSE;

  now = g_source_get_time (source);

  return (now > self->next_wakeup);
}

#if HAVE_IPHB
static inline guint
get_min_interval (
    WockyHeartbeatSource *self)
{
  /* We allow the heartbeat service to wake us up up to a minute early. */
  return self->max_interval > 60 ? self->max_interval - 60 : 0;
}
#endif

static gboolean
wocky_heartbeat_source_dispatch (
    GSource *source,
    GSourceFunc callback,
    gpointer user_data)
{
  WockyHeartbeatSource *self = (WockyHeartbeatSource *) source;

  if (callback == NULL)
    {
      g_warning ("No callback set for WockyHeartbeatSource %p", self);
      return FALSE;
    }

  /* Call our callback. We don't currently allow callbacks to stop future
   * heartbeats from occurring: this source is used for keepalives from the
   * time we're connected until we disconnect.
   */
  if (DEBUGGING)
    {
      gint64 now;

      now = g_source_get_time (source);
      DEBUG ("calling %p (%p) at %li", callback, user_data, now);
    }

  ((WockyHeartbeatCallback) callback) (user_data);

#if HAVE_IPHB
  wocky_heartbeat_source_wait (self, self->max_interval);
#endif

  /* Record the time we next want to wake up. */
  self->next_wakeup = g_source_get_time (source);
  self->next_wakeup += self->max_interval * G_USEC_PER_SEC;
  DEBUG ("next wakeup at %li", self->next_wakeup);

  return TRUE;
}

static void
wocky_heartbeat_source_finalize (GSource *source)
{
#ifdef HAVE_IPHB
  WockyHeartbeatSource *self = (WockyHeartbeatSource *) source;

  wocky_heartbeat_source_degrade (self);
#endif
}

static GSourceFuncs wocky_heartbeat_source_funcs = {
    wocky_heartbeat_source_prepare,
    wocky_heartbeat_source_check,
    wocky_heartbeat_source_dispatch,
    wocky_heartbeat_source_finalize,
    NULL,
    NULL
};

#if HAVE_IPHB
static void
connect_to_heartbeat (
    WockyHeartbeatSource *self)
{
  GSource *source = (GSource *) self;

  self->heartbeat = iphb_open (NULL);

  if (self->heartbeat == NULL)
    {
      DEBUG ("Couldn't open connection to heartbeat service: %s",
          g_strerror (errno));
      return;
    }

  self->fd.fd = iphb_get_fd (self->heartbeat);
  self->fd.events = G_IO_IN | G_IO_HUP | G_IO_ERR;
  g_source_add_poll (source, &self->fd);

  wocky_heartbeat_source_wait (self, self->max_interval);
}
#endif

/**
 * wocky_heartbeat_source_new:
 * @max_interval: the maximum interval between calls to the source's callback,
 *                in seconds. Pass 0 to prevent the callback being called.
 *
 * Creates a source which calls its callback at least every @max_interval
 * seconds. This is similar to g_timeout_source_new_seconds(), except that the
 * callback may be called slightly earlier than requested, in sync with other
 * periodic network activity (from other XMPP connections, or other
 * applications entirely).
 *
 * When calling g_source_set_callback() on this source, the supplied callback's
 * signature should match #WockyHeartbeatCallback.
 *
 * Returns: the newly-created source.
 */
GSource *
wocky_heartbeat_source_new (
    guint max_interval)
{
  GSource *source = g_source_new (&wocky_heartbeat_source_funcs,
      sizeof (WockyHeartbeatSource));
  WockyHeartbeatSource *self = (WockyHeartbeatSource *) source;

  /* We can't just call wocky_heartbeat_source_update_interval() because it
   * assumes that we're attached to a main context. I think this is probably a
   * reasonable assumption.
   */
  self->max_interval = max_interval;

  self->next_wakeup = g_get_monotonic_time ();
  self->next_wakeup += max_interval * G_USEC_PER_SEC;

#if HAVE_IPHB
  connect_to_heartbeat (self);
#endif

  return source;
}

/**
 * wocky_heartbeat_source_update_interval:
 * @source: a source returned by wocky_heartbeat_source_new()
 * @max_interval: the new maximum interval between calls to the source's
 *                callback, in seconds. Pass 0 to stop the callback being
 *                called.
 *
 * Updates the interval between calls to @source's callback. The new interval
 * may not take effect until after the next call to the callback.
 */
void
wocky_heartbeat_source_update_interval (
    GSource *source,
    guint max_interval)
{
  WockyHeartbeatSource *self = (WockyHeartbeatSource *) source;

  if (self->max_interval == max_interval)
    return;

  /* If we're not using the heartbeat, the new interval takes effect
   * immediately.
   *
   * If we are, we just wait for the next heartbeat to fire as
   * normal, and then use these new values when we ask it to wait again.
   * (Except if the heartbeat was previously disabled, or is being disabled, in
   * which case we have to be sure to schedule a wakeup, or cancel the pending
   * wakeup, respectively.)
   *
   * We could alternatively calculate the time already elapsed since we last
   * called iphb_wait(), and from that calculate how much longer we want to
   * wait with these new values, taking care to deal with the cases where one
   * or both of min_interval and max_interval have already passed. But life is
   * too short.
   */

#ifdef HAVE_IPHB
  /* We specify 0 as the lower bound here to give us a better chance of falling
   * into step with other connections, which may have started waiting at
   * slightly different times.
   */
  if (self->max_interval == 0 || max_interval == 0)
    wocky_heartbeat_source_wait (self, max_interval);
#endif

  /* If we were previously disabled, we need to re-initialize next_wakeup, not
   * just update it.
   */
  if (self->max_interval == 0)
    self->next_wakeup = g_source_get_time (source);

  /* If this moves self->next_wakeup into the past, then we'll wake up ASAP,
   * which is what we want.
   */
  self->next_wakeup += (max_interval - self->max_interval) * G_USEC_PER_SEC;
  self->max_interval = max_interval;

  if (self->max_interval == 0)
    DEBUG ("heartbeat disabled");
  else
    DEBUG ("next wakeup at or before %li", self->next_wakeup);
}