Commit 131434e6 authored by Bernd Zeimetz's avatar Bernd Zeimetz

Merge branch 'master' into jessie-backports

parents f4202746 1415fd2b
......@@ -14,23 +14,29 @@
CFLAGS ?= -O2 -Wall
# if your compiler doesn't support OpenMP, comment out this line
CC += -fopenmp
# if your compiler doesn't support OpenMP, comment out this line, or
# define OPENMP_FLAGS to be empty
OPENMP_FLAGS ?= -fopenmp
override CC += $(OPENMP_FLAGS)
GIT_VERSION := $(shell git describe --abbrev=6 --dirty --always || date +%F)
GVCFLAGS += -DGIT_VERSION=\"$(GIT_VERSION)\"
override CFLAGS += $(GVCFLAGS) `pkg-config openssl --cflags` -pthread
override CFLAGS += $(GVCFLAGS) -pthread
SOCKET_LIBS =
ifeq ($(shell uname), SunOS)
SOCKET_LIBS += -lsocket -lnsl
endif
override LIBS += `pkg-config openssl --libs` $(SOCKET_LIBS) -pthread
# should be accepted sort of anywhere
MATH_LIBS = -lm
override LIBS += $(SOCKET_LIBS) $(MATH_LIBS) -pthread
OBJS = \
relay.o \
md5.o \
consistent-hash.o \
receptor.o \
dispatcher.o \
......
This diff is collapsed.
This diff is collapsed.
......@@ -27,33 +27,43 @@
typedef struct _aggregator {
unsigned short interval; /* when to perform the aggregation */
unsigned short expire; /* when incoming metrics are no longer valid */
enum _aggr_timestamp { TS_START, TS_MIDDLE, TS_END } tswhen;
unsigned char bucketcnt;
size_t received;
size_t sent;
size_t dropped;
struct _aggr_computes {
enum _aggr_compute_type { SUM, CNT, MAX, MIN, AVG } type;
enum _aggr_compute_type { SUM, CNT, MAX, MIN, AVG,
MEDN, PCTL, VAR, SDEV } type;
const char *metric; /* name template of metric to produce */
struct _aggr_invocations {
char *metric; /* actual name to emit */
unsigned int hash; /* to speed up matching */
struct _bucket {
unsigned short expire; /* expire + splay */
struct _aggr_bucket {
long long int start;
size_t cnt;
double sum;
double max;
double min;
struct _aggr_bucket_entries {
size_t size;
double *values;
} entries;
} *buckets;
struct _aggr_invocations *next;
} *invocations_ht[1 << AGGR_HT_POW_SIZE];
unsigned char entries_needed:1;
unsigned char percentile:7;
struct _aggr_computes *next;
} *computes;
pthread_mutex_t bucketlock;
struct _aggregator *next;
} aggregator;
aggregator *aggregator_new(unsigned int interval, unsigned int expire);
aggregator *aggregator_new(unsigned int interval, unsigned int expire, enum _aggr_timestamp tswhen);
char aggregator_add_compute(aggregator *s, const char *metric, const char *type);
void aggregator_set_stub(aggregator *s, const char *stubname);
void aggregator_putmetric(aggregator *s, const char *metric, const char *firstspace, size_t nmatch, regmatch_t *pmatch);
int aggregator_start(server *submission);
void aggregator_stop(void);
......@@ -62,5 +72,8 @@ size_t aggregator_numcomputes(void);
size_t aggregator_get_received(void);
size_t aggregator_get_sent(void);
size_t aggregator_get_dropped(void);
size_t aggregator_get_received_sub(void);
size_t aggregator_get_sent_sub(void);
size_t aggregator_get_dropped_sub(void);
#endif
......@@ -45,11 +45,13 @@ collector_runner(void *s)
int i;
size_t totticks;
size_t totmetrics;
size_t totblackholes;
size_t totqueued;
size_t totstalls;
size_t totdropped;
size_t ticks;
size_t metrics;
size_t blackholes;
size_t queued;
size_t stalls;
size_t dropped;
......@@ -65,6 +67,16 @@ collector_runner(void *s)
char metric[METRIC_BUFSIZ];
char *m;
size_t sizem = 0;
size_t (*s_ticks)(server *);
size_t (*s_metrics)(server *);
size_t (*s_stalls)(server *);
size_t (*s_dropped)(server *);
size_t (*d_ticks)(dispatcher *);
size_t (*d_metrics)(dispatcher *);
size_t (*d_blackholes)(dispatcher *);
size_t (*a_received)(void);
size_t (*a_sent)(void);
size_t (*a_dropped)(void);
/* prepare hostname for graphite metrics */
snprintf(metric, sizeof(metric), "carbon.relays.%s", relay_hostname);
......@@ -75,8 +87,33 @@ collector_runner(void *s)
*m = '\0';
sizem = sizeof(metric) - (m - metric);
/* setup functions to target what the user wants */
if (debug & 2) {
s_ticks = server_get_ticks_sub;
s_metrics = server_get_metrics_sub;
s_stalls = server_get_stalls_sub;
s_dropped = server_get_dropped_sub;
d_ticks = dispatch_get_ticks_sub;
d_metrics = dispatch_get_metrics_sub;
d_blackholes = dispatch_get_blackholes_sub;
a_received = aggregator_get_received_sub;
a_sent = aggregator_get_sent_sub;
a_dropped = aggregator_get_dropped_sub;
} else {
s_ticks = server_get_ticks;
s_metrics = server_get_metrics;
s_stalls = server_get_stalls;
s_dropped = server_get_dropped;
d_ticks = dispatch_get_ticks;
d_metrics = dispatch_get_metrics;
d_blackholes = dispatch_get_blackholes;
a_received = aggregator_get_received;
a_sent = aggregator_get_sent;
a_dropped = aggregator_get_dropped;
}
#define send(metric) \
if (debug) \
if (debug & 1) \
logout("%s", metric); \
else \
server_send(submission, strdup(metric), 1);
......@@ -98,6 +135,7 @@ collector_runner(void *s)
nextcycle += collector_interval;
totticks = 0;
totmetrics = 0;
totblackholes = 0;
dispatchers_idle = 0;
dispatchers_busy = 0;
for (i = 0; dispatchers[i] != NULL; i++) {
......@@ -106,11 +144,15 @@ collector_runner(void *s)
} else {
dispatchers_idle++;
}
totticks += ticks = dispatch_get_ticks(dispatchers[i]);
totmetrics += metrics = dispatch_get_metrics(dispatchers[i]);
totticks += ticks = d_ticks(dispatchers[i]);
totmetrics += metrics = d_metrics(dispatchers[i]);
totblackholes += blackholes = d_blackholes(dispatchers[i]);
snprintf(m, sizem, "dispatcher%d.metricsReceived %zd %zd\n",
i + 1, metrics, (size_t)now);
send(metric);
snprintf(m, sizem, "dispatcher%d.metricsBlackholed %zd %zd\n",
i + 1, blackholes, (size_t)now);
send(metric);
snprintf(m, sizem, "dispatcher%d.wallTime_us %zd %zd\n",
i + 1, ticks, (size_t)now);
send(metric);
......@@ -118,6 +160,9 @@ collector_runner(void *s)
snprintf(m, sizem, "metricsReceived %zd %zd\n",
totmetrics, (size_t)now);
send(metric);
snprintf(m, sizem, "metricsBlackholed %zd %zd\n",
totblackholes, (size_t)now);
send(metric);
snprintf(m, sizem, "dispatch_wallTime_us %zd %zd\n",
totticks, (size_t)now);
send(metric);
......@@ -137,11 +182,11 @@ collector_runner(void *s)
if (server_ctype(srvs[i]) == CON_PIPE) {
strncpy(ipbuf, "internal", sizeof(ipbuf));
ticks = server_get_ticks(srvs[i]);
metrics = server_get_metrics(srvs[i]);
ticks = s_ticks(srvs[i]);
metrics = s_metrics(srvs[i]);
queued = server_get_queue_len(srvs[i]);
stalls = server_get_stalls(srvs[i]);
dropped = server_get_dropped(srvs[i]);
stalls = s_stalls(srvs[i]);
dropped = s_dropped(srvs[i]);
} else {
snprintf(ipbuf, sizeof(ipbuf), "%s:%u",
server_ip(srvs[i]), server_port(srvs[i]));
......@@ -149,11 +194,11 @@ collector_runner(void *s)
if (*p == '.')
*p = '_';
totticks += ticks = server_get_ticks(srvs[i]);
totmetrics += metrics = server_get_metrics(srvs[i]);
totticks += ticks = s_ticks(srvs[i]);
totmetrics += metrics = s_metrics(srvs[i]);
totqueued += queued = server_get_queue_len(srvs[i]);
totstalls += stalls = server_get_stalls(srvs[i]);
totdropped += dropped = server_get_dropped(srvs[i]);
totstalls += stalls = s_stalls(srvs[i]);
totdropped += dropped = s_dropped(srvs[i]);
}
snprintf(m, sizem, "destinations.%s.sent %zd %zd\n",
ipbuf, metrics, (size_t)now);
......@@ -196,17 +241,17 @@ collector_runner(void *s)
if (numaggregators > 0) {
snprintf(m, sizem, "aggregators.metricsReceived %zd %zd\n",
aggregator_get_received(), (size_t)now);
a_received(), (size_t)now);
send(metric);
snprintf(m, sizem, "aggregators.metricsSent %zd %zd\n",
aggregator_get_sent(), (size_t)now);
a_sent(), (size_t)now);
send(metric);
snprintf(m, sizem, "aggregators.metricsDropped %zd %zd\n",
aggregator_get_dropped(), (size_t)now);
a_dropped(), (size_t)now);
send(metric);
}
if (debug)
if (debug & 1)
fflush(stdout);
}
......@@ -223,6 +268,8 @@ collector_writer(void *unused)
{
int i = 0;
size_t queued;
size_t queuesize;
double queueusage;
size_t totdropped;
size_t numaggregators = aggregator_numaggregators();
server **srvs = NULL;
......@@ -243,12 +290,15 @@ collector_writer(void *unused)
totdropped = 0;
for (i = 0; srvs[i] != NULL; i++) {
queued = server_get_queue_len(srvs[i]);
queuesize = server_get_queue_size(srvs[i]);
totdropped += server_get_dropped(srvs[i]);
queueusage = (double)queued / (double)queuesize;
if (queued > 150)
if (queueusage >= 0.75)
logout("warning: metrics queuing up "
"for %s:%u: %zd metrics\n",
server_ip(srvs[i]), server_port(srvs[i]), queued);
"for %s:%u: %zd metrics (%d%% of queue size)\n",
server_ip(srvs[i]), server_port(srvs[i]), queued,
(int)(queueusage * 100));
}
if (totdropped - lastdropped > 0)
logout("warning: dropped %zd metrics\n", totdropped - lastdropped);
......@@ -291,13 +341,14 @@ collector_reloadcomplete(void)
* Initialises and starts the collector.
*/
void
collector_start(dispatcher **d, cluster *c, server *submission)
collector_start(dispatcher **d, cluster *c, server *submission, char cum)
{
dispatchers = d;
collector_schedulereload(c);
if (mode == DEBUG)
if (mode == DEBUG || mode == DEBUGTEST)
debug = 1;
debug |= (cum ? 0 : 2);
if (mode != SUBMISSION) {
if (pthread_create(&collectorid, NULL, collector_runner, submission) != 0)
......
......@@ -28,7 +28,7 @@ extern int collector_interval;
#define timediff(X, Y) \
(Y.tv_sec > X.tv_sec ? (Y.tv_sec - X.tv_sec) * 1000 * 1000 + ((Y.tv_usec - X.tv_usec)) : Y.tv_usec - X.tv_usec)
void collector_start(dispatcher **d, cluster *c, server *submission);
void collector_start(dispatcher **d, cluster *c, server *submission, char cum);
void collector_stop(void);
void collector_schedulereload(cluster *c);
char collector_reloadcomplete(void);
......
......@@ -18,9 +18,10 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/md5.h>
#include <assert.h>
#include "fnv1a.h"
#include "md5.h"
#include "server.h"
#define CH_RING struct _ch_ring
......@@ -69,21 +70,68 @@ carbon_hashpos(const char *key, const char *end)
static unsigned short
fnv1a_hashpos(const char *key, const char *end)
{
unsigned int hash = 2166136261UL; /* FNV1a */
unsigned int hash;
for (; key < end; key++)
hash = (hash ^ (unsigned int)*key) * 16777619;
fnv1a_32(hash, key, key, end);
return (unsigned short)((hash >> 16) ^ (hash & (unsigned int)0xFFFF));
}
/**
* Qsort comparator for ch_ring_entry structs on pos.
* Sort comparator for ch_ring_entry structs on pos, ip and instance.
*/
static int
entrycmp(const void *l, const void *r)
entrycmp_carbon(const void *l, const void *r)
{
return ((ch_ring_entry *)l)->pos - ((ch_ring_entry *)r)->pos;
ch_ring_entry *ch_l = (ch_ring_entry *)l;
ch_ring_entry *ch_r = (ch_ring_entry *)r;
if (ch_l->pos != ch_r->pos)
return ch_l->pos - ch_r->pos;
#ifndef CH_CMP_V40_BEHAVIOUR
{
int d = strcmp(server_ip(ch_l->server), server_ip(ch_r->server));
char *i_l, *i_r;
if (d != 0)
return d;
i_l = server_instance(ch_l->server);
i_r = server_instance(ch_r->server);
if (i_l == NULL && i_r == NULL)
return 0;
if (i_l == NULL)
return 1;
if (i_r == NULL)
return -1;
return strcmp(i_l, i_r);
}
#endif
return 0;
}
/**
* Sort comparator for ch_ring_entry structs on pos, ip and port.
*/
static int
entrycmp_fnv1a(const void *l, const void *r)
{
ch_ring_entry *ch_l = (ch_ring_entry *)l;
ch_ring_entry *ch_r = (ch_ring_entry *)r;
if (ch_l->pos != ch_r->pos)
return ch_l->pos - ch_r->pos;
#ifndef CH_CMP_V40_BEHAVIOUR
{
int d = strcmp(server_ip(ch_l->server), server_ip(ch_r->server));
if (d != 0)
return d;
return server_port(ch_l->server) - server_port(ch_r->server);
}
#endif
return 0;
}
ch_ring *
......@@ -114,6 +162,8 @@ ch_addnode(ch_ring *ring, server *s)
int i;
char buf[256];
ch_ring_entry *entries;
char *instance = server_instance(s);
int (*cmp)(const void *, const void *) = NULL;
if (ring == NULL)
return NULL;
......@@ -126,7 +176,6 @@ ch_addnode(ch_ring *ring, server *s)
switch (ring->type) {
case CARBON:
for (i = 0; i < ring->hash_replicas; i++) {
char *instance = server_instance(s);
/* this format is actually Python's tuple format that is
* used in serialised form as input for the hash */
snprintf(buf, sizeof(buf), "('%s', %s%s%s):%d",
......@@ -135,33 +184,41 @@ ch_addnode(ch_ring *ring, server *s)
instance == NULL ? "None" : instance,
instance == NULL ? "" : "'",
i);
/* TODO:
/* carbon upstream committed:
* https://github.com/graphite-project/carbon/commit/024f9e67ca47619438951c59154c0dec0b0518c7
* Question is how harmful the collision is -- it will probably
* change the location of some metrics */
* this makes sure no collissions exist on pos, however,
* at the expense of being agnostic to the input order,
* therefore that change isn't implemented here, see
* https://github.com/grobian/carbon-c-relay/issues/84 */
entries[i].pos = carbon_hashpos(buf, buf + strlen(buf));
entries[i].server = s;
entries[i].next = NULL;
entries[i].malloced = 0;
}
cmp = *entrycmp_carbon;
break;
case FNV1a:
for (i = 0; i < ring->hash_replicas; i++) {
/* take all server info into account, such that
* different port numbers for the same hosts will work
* (unlike CARBON) */
snprintf(buf, sizeof(buf), "%d-%s:%u",
i, server_ip(s), server_port(s));
* (unlike CARBON), unless we got a full overrride */
if (instance == NULL) {
snprintf(buf, sizeof(buf), "%d-%s:%u",
i, server_ip(s), server_port(s));
} else {
snprintf(buf, sizeof(buf), "%d-%s", i, instance);
}
entries[i].pos = fnv1a_hashpos(buf, buf + strlen(buf));
entries[i].server = s;
entries[i].next = NULL;
entries[i].malloced = 0;
}
cmp = *entrycmp_fnv1a;
break;
}
/* sort to allow merge joins later down the road */
qsort(entries, ring->hash_replicas, sizeof(ch_ring_entry), *entrycmp);
qsort(entries, ring->hash_replicas, sizeof(ch_ring_entry), cmp);
entries[0].malloced = 1;
if (ring->entries == NULL) {
......@@ -175,7 +232,7 @@ ch_addnode(ch_ring *ring, server *s)
last = NULL;
assert(ring->hash_replicas > 0);
for (w = ring->entries; w != NULL && i < ring->hash_replicas; ) {
if (w->pos < entries[i].pos) {
if (cmp(&w->pos, &entries[i].pos) <= 0) {
last = w;
w = w->next;
} else {
......@@ -256,6 +313,45 @@ ch_get_nodes(
}
}
void
ch_printhashring(ch_ring *ring, FILE *f)
{
ch_ring_entry *w;
char column = 0;
char srvbuf[21];
for (w = ring->entries; w != NULL; w = w->next) {
snprintf(srvbuf, sizeof(srvbuf), "%s:%d%s%s",
server_ip(w->server),
server_port(w->server),
server_instance(w->server) ? "=" : "",
server_instance(w->server) ? server_instance(w->server) : "");
fprintf(f, "%5d@%-20s", w->pos, srvbuf);
if (column < 2) {
fprintf(f, " ");
column++;
} else {
fprintf(f, "\n");
column = 0;
}
}
}
unsigned short
ch_gethashpos(ch_ring *ring, const char *key, const char *end)
{
switch (ring->type) {
case CARBON:
return carbon_hashpos(key, end);
case FNV1a:
return fnv1a_hashpos(key, end);
default:
assert(0); /* this shouldn't happen */
}
return 0; /* pacify compiler */
}
/**
* Frees the ring structure and its added nodes.
*/
......
......@@ -18,6 +18,8 @@
#ifndef CONSISTENT_HASH_H
#define CONSISTENT_HASH_H 1
#include <stdio.h>
#include "server.h"
#include "router.h"
......@@ -35,6 +37,8 @@ void ch_get_nodes(
const char replcnt,
const char *metric,
const char *firstspace);
void ch_printhashring(ch_ring *ring, FILE *out);
unsigned short ch_gethashpos(ch_ring *ring, const char *key, const char *end);
void ch_free(ch_ring *ring);
#endif
../../LICENSE.md
\ No newline at end of file
# -----------------------------------------------------------------------------
# Makefile for carbon-c-relay
#
# Author: Jose Riguera <jriguera@gmail.com> based on the work of Fabian Groffen
# Date : 2014-10-12
#
# -----------------------------------------------------------------------------
# creation of initial debian folder:
# dh_make --native -c apache -s -p carbon-c-relay_0.35
CC = gcc
LINKER = gcc -o
RM = rm -f
MD = mkdir -p
GIT = git
INSTALL = install
DEBUILD = debuild
DEBCLEAN = debclean
# project name (generate executable with this name)
DESTDIR = /usr/local/etc/carbon
PREFIX = /usr/local
TARGET = carbon-c-relay
GIT_VERSION := $(shell git describe --abbrev=6 --dirty --always || date +%F)
GVCFLAGS += -DGIT_VERSION=\"$(GIT_VERSION)\"
# change these to set the proper directories where each files shoould be
SRCDIR = src
OBJDIR = obj
BINDIR = sbin
# compiling flags here
CFLAGS ?= -O2 -Wall
override CFLAGS += $(GVCFLAGS) `pkg-config openssl --cflags` -pthread
# linking flags here
override LIBS += `pkg-config openssl --libs` -pthread
ifeq ($(shell uname), SunOS)
override LIBS += -lsocket -lnsl
endif
LFLAGS = -O2 -Wall -lm $(LIBS)
SOURCES := $(wildcard $(SRCDIR)/*.c)
INCLUDES := $(wildcard $(SRCDIR)/*.h)
OBJECTS := $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o)
all: folders $(BINDIR)/$(TARGET)
$(BINDIR)/$(TARGET): $(OBJECTS)
$(LINKER) $@ $(OBJECTS) $(LFLAGS)
@echo "Linking complete. Binary file created!"
$(OBJECTS): $(OBJDIR)/%.o : $(SRCDIR)/%.c
$(CC) $(CFLAGS) -c $< -o $@
@echo "Compiled "$<" successfully!"
.PHONY: folders
folders:
@$(MD) $(BINDIR)
@$(MD) $(OBJDIR)
.PHONY: install
install: $(BINDIR)/$(TARGET)
$(INSTALL) -m 0755 $< $(PREFIX)/$(BINDIR)/$(TARGET)
$(INSTALL) -d $(DESTDIR)
$(INSTALL) -m 0644 conf/* $(DESTDIR)
.PHONY: uninstall
uninstall:
$(RM) $(PREFIX)/$(BINDIR)/$(TARGET)
$(RM) -rf $(DESTDIR)/etc/$(TARGET)
VERSION = $(shell sed -n '/VERSION/s/^.*"\([0-9.]\+\)".*$$/\1/p' $(SRCDIR)/relay.h)
.PHONY: dist
dist:
@$(GIT) archive \
--format=tar.gz \
--prefix=$(TARGET)-$(VERSION)/ v$(VERSION) \
> $(TARGET)-$(VERSION).tar.gz
@echo "Created $(TARGET)-$(VERSION).tar.gz successfully!"
# check if the local environment is suitable to generate a package
# we check environment variables and a gpg private key matching
# these variables. this is necessary as we sign our packages.
.PHONY: checkenv
checkenv:
ifndef DEBEMAIL
echo "Missing environment variable DEBEMAIL"
@exit 1
endif
ifndef DEBFULLNAME
echo "Missing environment variable DEBFULLNAME"
@exit 1
endif
#gpg --list-secret-keys "$(DEBFULLNAME) <$(DEBEMAIL)>" >/dev/null
# creates the .deb package and other related files
# all files are placed in ../
.PHONY: builddeb
debuild: checkenv
# dpkg-buildpackage + lintian
# -sa Forces the inclusion of the original source.
# -us Do not sign the source package.
# -uc Do not sign the .changes file.
$(DEBUILD) -us -uc -sa -b -rfakeroot -i -I.git -I.gitignore -I$(BINDIR) -I$(OBJDIR) -I$(TARGET)-$(VERSION).tar.gz
# create a new release based on PW_VERSION variable
.PHONY: newrelease
newrelease:
debchange --changelog debian/changelog --urgency high --newversion $(VERSION)-1 "Releasing $(TARGET) $(VERSION)"
# creates a new version in debian/changelog
.PHONY: newversion
newversion:
debchange --changelog debian/changelog -i --urgency high
# allow user to enter one or more changelog comment manually
.PHONY: changelog
changelog:
debchange --changelog debian/changelog --force-distribution dist --urgency high -r
debchange --changelog debian/changelog -a
.PHONY: clean
clean:
@$(RM) $(OBJECTS)
@echo "Cleanup complete!"
.PHONY: dist-clean
distclean: clean
@$(RM) -r $(BINDIR)
@$(RM) -r $(OBJDIR)
@$(RM) $(TARGET)-$(VERSION).tar.gz
@echo "Executable removed!"
../../README.md
\ No newline at end of file
# comments are allowed in any place and start with a hash (#)
#
#cluster <name>
# <forward | any_of | <carbon_ch | fnv1a_ch> [replication <count>]>
# <host[:port] [proto <udp | tcp>]> ...
# ;
#
#rewrite <expression>
# into <replacement>
# ;
#
#aggregate
# <expression> ...
# every <interval> seconds
# expire after <expiration> seconds
# compute <sum | count | max | min | average> write to
# <metric>
# [compute ...]
# ;
#
#match <* | <expression>>
# send to <cluster | blackhole>
# [stop]
# ;
#
# example configuration for simply forwarding everything to a locally
# running process that listens on port 2013:
cluster local_carbon
forward
127.0.0.1:2013
;
match *
send to local_carbon
stop
;
The Debian Package carbon-c-relay
----------------------------
carbon-c-relay
-- Jose Riguera <jriguera@gmail.com> Mon, 13 Oct 2014 00:07:24 +0200
carbon-c-relay for Debian
-------------------------
-- Jose Riguera <jriguera@gmail.com> Mon, 13 Oct 2014 00:07:24 +0200
carbon-c-relay for Debian
-------------------------
https://github.com/grobian/carbon-c-relay
-- Jose Riguera <jriguera@gmail.com> Mon, 13 Oct 2014 00:07:24 +0200