libmsc: Track and update the location update expiry

Set the subscriber expiry timeout to twice the duration of the location
update period and provide functions subscr_expire() and
db_subscriber_expire() to mark subscribers offline that have missed two
location update periods.

This patch increases the DB revision to 3, so the hlr will be
incompatible with prior versions.

We should allow 0 for T3212 as well to disable the location update
period. In that case we will need a way to indicate that in the
database.
diff --git a/openbsc/include/openbsc/db.h b/openbsc/include/openbsc/db.h
index d0c85ea..25c2aea 100644
--- a/openbsc/include/openbsc/db.h
+++ b/openbsc/include/openbsc/db.h
@@ -41,6 +41,7 @@
 					 enum gsm_subscriber_field field,
 					 const char *subscr);
 int db_sync_subscriber(struct gsm_subscriber *subscriber);
+int db_subscriber_expire(void *priv, void (*callback)(void *priv, long long unsigned int id));
 int db_subscriber_alloc_tmsi(struct gsm_subscriber *subscriber);
 int db_subscriber_alloc_exten(struct gsm_subscriber *subscriber);
 int db_subscriber_alloc_token(struct gsm_subscriber *subscriber, uint32_t* token);
diff --git a/openbsc/include/openbsc/gsm_data.h b/openbsc/include/openbsc/gsm_data.h
index ea9f601..feb692f 100644
--- a/openbsc/include/openbsc/gsm_data.h
+++ b/openbsc/include/openbsc/gsm_data.h
@@ -247,6 +247,9 @@
 	int T3122;
 	int T3141;
 
+	/* timer to expire old location updates */
+	struct osmo_timer_list subscr_expire_timer;
+
 	/* Radio Resource Location Protocol (TS 04.31) */
 	struct {
 		enum rrlp_mode mode;
diff --git a/openbsc/include/openbsc/gsm_subscriber.h b/openbsc/include/openbsc/gsm_subscriber.h
index 6cf8573..78f9710 100644
--- a/openbsc/include/openbsc/gsm_subscriber.h
+++ b/openbsc/include/openbsc/gsm_subscriber.h
@@ -38,6 +38,7 @@
 	char name[GSM_NAME_LENGTH];
 	char extension[GSM_EXTENSION_LENGTH];
 	int authorized;
+	time_t expire_lu;
 
 	/* Temporary field which is not stored in the DB/HLR */
 	uint32_t flags;
@@ -98,6 +99,7 @@
 
 int subscr_purge_inactive(struct gsm_network *net);
 void subscr_update_from_db(struct gsm_subscriber *subscr);
+void subscr_expire(struct gsm_network *net);
 
 /* internal */
 struct gsm_subscriber *subscr_alloc(void);
diff --git a/openbsc/src/libmsc/db.c b/openbsc/src/libmsc/db.c
index 9a5f18d..4e20e23 100644
--- a/openbsc/src/libmsc/db.c
+++ b/openbsc/src/libmsc/db.c
@@ -42,6 +42,8 @@
 static char *db_dirname = NULL;
 static dbi_conn conn;
 
+#define SCHEMA_REVISION "3"
+
 static char *create_stmts[] = {
 	"CREATE TABLE IF NOT EXISTS Meta ("
 		"id INTEGER PRIMARY KEY AUTOINCREMENT, "
@@ -51,7 +53,7 @@
 	"INSERT OR IGNORE INTO Meta "
 		"(key, value) "
 		"VALUES "
-		"('revision', '2')",
+		"('revision', " SCHEMA_REVISION ")",
 	"CREATE TABLE IF NOT EXISTS Subscriber ("
 		"id INTEGER PRIMARY KEY AUTOINCREMENT, "
 		"created TIMESTAMP NOT NULL, "
@@ -61,7 +63,8 @@
 		"extension TEXT UNIQUE, "
 		"authorized INTEGER NOT NULL DEFAULT 0, "
 		"tmsi TEXT UNIQUE, "
-		"lac INTEGER NOT NULL DEFAULT 0"
+		"lac INTEGER NOT NULL DEFAULT 0, "
+		"expire_lu TIMESTAMP DEFAULT NULL"
 		")",
 	"CREATE TABLE IF NOT EXISTS AuthToken ("
 		"id INTEGER PRIMARY KEY AUTOINCREMENT, "
@@ -158,10 +161,39 @@
 	LOGP(DDB, LOGL_ERROR, "DBI: %s\n", msg);
 }
 
+static int update_db_revision_2(void)
+{
+	dbi_result result;
+
+	result = dbi_conn_query(conn,
+				"ALTER TABLE Subscriber "
+				"ADD COLUMN expire_lu "
+				"TIMESTAMP DEFAULT NULL");
+	if (!result) {
+		LOGP(DDB, LOGL_ERROR,
+		     "Failed to alter table Subscriber (upgrade vom rev 2).\n");
+		return -EINVAL;
+	}
+	dbi_result_free(result);
+
+	result = dbi_conn_query(conn,
+				"UPDATE Meta "
+				"SET value = '3' "
+				"WHERE key = 'revision'");
+	if (!result) {
+		LOGP(DDB, LOGL_ERROR,
+		     "Failed set new revision (upgrade vom rev 2).\n");
+		return -EINVAL;
+	}
+	dbi_result_free(result);
+
+	return 0;
+}
+
 static int check_db_revision(void)
 {
 	dbi_result result;
-	const char *rev;
+	const char *rev_s;
 
 	result = dbi_conn_query(conn,
 				"SELECT value FROM Meta WHERE key='revision'");
@@ -172,11 +204,37 @@
 		dbi_result_free(result);
 		return -EINVAL;
 	}
-	rev = dbi_result_get_string(result, "value");
-	if (!rev || atoi(rev) != 2) {
+	rev_s = dbi_result_get_string(result, "value");
+	if (!rev_s) {
 		dbi_result_free(result);
 		return -EINVAL;
 	}
+	if (!strcmp(rev_s, "2")) {
+		if (update_db_revision_2()) {
+			LOGP(DDB, LOGL_FATAL, "Failed to update database from schema revision '%s'.\n", rev_s);
+			dbi_result_free(result);
+			return -EINVAL;
+		}
+	} else if (!strcmp(rev_s, SCHEMA_REVISION)) {
+		/* everything is fine */
+	} else {
+		LOGP(DDB, LOGL_FATAL, "Invalid database schema revision '%s'.\n", rev_s);
+		dbi_result_free(result);
+		return -EINVAL;
+	}
+
+	dbi_result_free(result);
+	return 0;
+}
+
+static int db_configure(void)
+{
+	dbi_result result;
+
+	result = dbi_conn_query(conn,
+				"PRAGMA synchronous = FULL");
+	if (!result)
+		return -EINVAL;
 
 	dbi_result_free(result);
 	return 0;
@@ -242,6 +300,8 @@
                 return -1;
 	}
 
+	db_configure();
+
 	return 0;
 }
 
@@ -575,6 +635,12 @@
 		strncpy(subscr->extension, string, GSM_EXTENSION_LENGTH);
 
 	subscr->lac = dbi_result_get_uint(result, "lac");
+
+	if (!dbi_result_field_is_null(result, "expire_lu"))
+		subscr->expire_lu = dbi_result_get_datetime(result, "expire_lu");
+	else
+		subscr->expire_lu = 0;
+
 	subscr->authorized = dbi_result_get_uint(result, "authorized");
 }
 
@@ -707,13 +773,15 @@
 		"extension = %s, "
 		"authorized = %i, "
 		"tmsi = %s, "
-		"lac = %i "
+		"lac = %i, "
+		"expire_lu = datetime(%i, 'unixepoch') "
 		"WHERE imsi = %s ",
 		q_name,
 		q_extension,
 		subscriber->authorized,
 		q_tmsi,
 		subscriber->lac,
+		subscriber->expire_lu,
 		subscriber->imsi);
 
 	free(q_tmsi);
@@ -776,6 +844,29 @@
 	return 0;
 }
 
+int db_subscriber_expire(void *priv, void (*callback)(void *priv, long long unsigned int id))
+{
+	dbi_result result;
+
+	result = dbi_conn_query(conn,
+			"SELECT id "
+			"FROM Subscriber "
+			"WHERE lac != 0 AND "
+				"( expire_lu is NULL "
+				"OR expire_lu < datetime('now') ) "
+			"LIMIT 1");
+	if (!result) {
+		LOGP(DDB, LOGL_ERROR, "Failed to get expired subscribers\n");
+		return -EIO;
+	}
+
+	while (dbi_result_next_row(result))
+		callback(priv, dbi_result_get_ulonglong(result, "id"));
+
+	dbi_result_free(result);
+	return 0;
+}
+
 int db_subscriber_alloc_tmsi(struct gsm_subscriber *subscriber)
 {
 	dbi_result result = NULL;
diff --git a/openbsc/src/libmsc/gsm_subscriber.c b/openbsc/src/libmsc/gsm_subscriber.c
index 0889400..3e65176 100644
--- a/openbsc/src/libmsc/gsm_subscriber.c
+++ b/openbsc/src/libmsc/gsm_subscriber.c
@@ -25,6 +25,7 @@
 #include <stdio.h>
 #include <string.h>
 #include <assert.h>
+#include <time.h>
 
 #include <osmocom/core/talloc.h>
 
@@ -333,6 +334,21 @@
 		s->net = bts->network;
 		/* Indicate "attached to LAC" */
 		s->lac = bts->location_area_code;
+
+		/* FIXME: We should allow 0 for T3212 as well to disable the
+		 * location update period. In that case we will need a way to
+		 * indicate that in the database and then reenable that value in
+		 * VTY.
+		 */
+
+		/* Table 10.5.33: The T3212 timeout value field is coded as the
+		 * binary representation of the timeout value for
+		 * periodic updating in decihours. Mark the subscriber as
+		 * inactive if it missed two consecutive location updates.
+		 * Timeout is twice the t3212 value plus one minute */
+		s->expire_lu = time(NULL) +
+			(bts->si_common.chan_desc.t3212 * 60 * 6 * 2) + 60;
+
 		LOGP(DMM, LOGL_INFO, "Subscriber %s ATTACHED LAC=%u\n",
 			subscr_name(s), s->lac);
 		rc = db_sync_subscriber(s);
@@ -364,6 +380,25 @@
 	db_subscriber_update(sub);
 }
 
+static void subscr_expire_callback(void *data, long long unsigned int id)
+{
+	struct gsm_network *net = data;
+	struct gsm_subscriber *s =
+		subscr_get_by_id(net, id);
+
+	LOGP(DMM, LOGL_NOTICE, "Expiring inactive subscriber %s (ID %i)\n",
+			subscr_name(s), id);
+	s->lac = GSM_LAC_RESERVED_DETACHED;
+	db_sync_subscriber(s);
+
+	subscr_put(s);
+}
+
+void subscr_expire(struct gsm_network *net)
+{
+	db_subscriber_expire(net, subscr_expire_callback);
+}
+
 int subscr_pending_requests(struct gsm_subscriber *sub)
 {
 	struct subscr_request *req;