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/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;