ms: Add timer

Currently the MS object is immediately idle when all TBFs are
detached and if no guard is being used. Since the plan is to use the
MS objects to pass information from one TBF to the next one even
across the gap of some seconds of inactivity, a mechanism is needed
to keep the MS objects around for some time.

This commit extends the GprsMs class by a timer that keeps the MS
objects in non-idle state for some time after all TBFs have been
detached. The set_timeout method must be used with a non-zero value
to activate this feature.

Sponsored-by: On-Waves ehf
diff --git a/src/gprs_ms.cpp b/src/gprs_ms.cpp
index d6520c3..f5b92d2 100644
--- a/src/gprs_ms.cpp
+++ b/src/gprs_ms.cpp
@@ -40,11 +40,9 @@
 
 static GprsMsDefaultCallback gprs_default_cb;
 
-
-GprsMs::Guard::Guard(GprsMs *ms) : m_ms(ms)
+GprsMs::Guard::Guard(GprsMs *ms) :
+	m_ms(ms ? ms->ref() : NULL)
 {
-	if (m_ms)
-		m_ms->ref();
 }
 
 GprsMs::Guard::~Guard()
@@ -53,6 +51,19 @@
 		m_ms->unref();
 }
 
+void GprsMs::timeout(void *priv_)
+{
+	GprsMs *ms = static_cast<GprsMs *>(priv_);
+
+	LOGP(DRLCMAC, LOGL_INFO, "Timeout for MS object, TLLI = 0x%08x\n",
+		ms->tlli());
+
+	if (ms->m_timer.data) {
+		ms->m_timer.data = NULL;
+		ms->unref();
+	}
+}
+
 GprsMs::GprsMs(uint32_t tlli) :
 	m_cb(&gprs_default_cb),
 	m_ul_tbf(NULL),
@@ -68,12 +79,17 @@
 	LOGP(DRLCMAC, LOGL_INFO, "Creating MS object, TLLI = 0x%08x\n", tlli);
 
 	m_imsi[0] = 0;
+	memset(&m_timer, 0, sizeof(m_timer));
+	m_timer.cb = GprsMs::timeout;
 }
 
 GprsMs::~GprsMs()
 {
 	LOGP(DRLCMAC, LOGL_INFO, "Destroying MS object, TLLI = 0x%08x\n", tlli());
 
+	if (osmo_timer_pending(&m_timer))
+		osmo_timer_del(&m_timer);
+
 	if (m_ul_tbf) {
 		m_ul_tbf->set_ms(NULL);
 		m_ul_tbf = NULL;
@@ -99,9 +115,10 @@
 	talloc_free(p);
 }
 
-void GprsMs::ref()
+GprsMs *GprsMs::ref()
 {
 	m_ref += 1;
+	return this;
 }
 
 void GprsMs::unref()
@@ -112,6 +129,27 @@
 		update_status();
 }
 
+void GprsMs::start_timer()
+{
+	if (m_delay == 0)
+		return;
+
+	if (!m_timer.data)
+		m_timer.data = ref();
+
+	osmo_timer_schedule(&m_timer, m_delay, 0);
+}
+
+void GprsMs::stop_timer()
+{
+	if (!m_timer.data)
+		return;
+
+	osmo_timer_del(&m_timer);
+	m_timer.data = NULL;
+	unref();
+}
+
 void GprsMs::attach_tbf(struct gprs_rlcmac_tbf *tbf)
 {
 	if (tbf->direction == GPRS_RLCMAC_DL_TBF)
@@ -134,6 +172,9 @@
 		detach_tbf(m_ul_tbf);
 
 	m_ul_tbf = tbf;
+
+	if (tbf)
+		stop_timer();
 }
 
 void GprsMs::attach_dl_tbf(struct gprs_rlcmac_dl_tbf *tbf)
@@ -150,6 +191,9 @@
 		detach_tbf(m_dl_tbf);
 
 	m_dl_tbf = tbf;
+
+	if (tbf)
+		stop_timer();
 }
 
 void GprsMs::detach_tbf(gprs_rlcmac_tbf *tbf)
@@ -167,6 +211,9 @@
 	if (tbf->ms() == this)
 		tbf->set_ms(NULL);
 
+	if (!m_dl_tbf && !m_dl_tbf)
+		start_timer();
+
 	update_status();
 }
 
diff --git a/src/gprs_ms.h b/src/gprs_ms.h
index 9c3acb4..db55bad 100644
--- a/src/gprs_ms.h
+++ b/src/gprs_ms.h
@@ -25,6 +25,11 @@
 struct gprs_rlcmac_ul_tbf;
 
 #include "cxx_linuxlist.h"
+
+extern "C" {
+	#include <osmocom/core/timer.h>
+}
+
 #include <stdint.h>
 #include <stddef.h>
 
@@ -62,6 +67,8 @@
 	uint8_t ta() const;
 	void set_ta(uint8_t ta);
 
+	void set_timeout(unsigned secs);
+
 	void attach_tbf(gprs_rlcmac_tbf *tbf);
 	void attach_ul_tbf(gprs_rlcmac_ul_tbf *tbf);
 	void attach_dl_tbf(gprs_rlcmac_dl_tbf *tbf);
@@ -76,10 +83,15 @@
 	LListHead<GprsMs>& list() {return this->m_list;}
 	const LListHead<GprsMs>& list() const {return this->m_list;}
 
+	/* internal use */
+	static void timeout(void *priv_);
+
 protected:
 	void update_status();
-	void ref();
+	GprsMs *ref();
 	void unref();
+	void start_timer();
+	void stop_timer();
 
 private:
 	Callback * m_cb;
@@ -96,6 +108,8 @@
 	bool m_is_idle;
 	int m_ref;
 	LListHead<GprsMs> m_list;
+	struct osmo_timer_list m_timer;
+	unsigned m_delay;
 };
 
 inline uint32_t GprsMs::tlli() const
@@ -120,3 +134,8 @@
 {
 	return m_ta;
 }
+
+inline void GprsMs::set_timeout(unsigned secs)
+{
+	m_delay = secs;
+}
diff --git a/tests/ms/MsTest.cpp b/tests/ms/MsTest.cpp
index 9c8ec2c..bb9e21b 100644
--- a/tests/ms/MsTest.cpp
+++ b/tests/ms/MsTest.cpp
@@ -36,6 +36,7 @@
 }
 
 #include <errno.h>
+#include <unistd.h>
 
 void *tall_pcu_ctx;
 int16_t spoof_mnc = 0, spoof_mcc = 0;
@@ -399,6 +400,74 @@
 	printf("=== end %s ===\n", __func__);
 }
 
+static void test_ms_timeout()
+{
+	uint32_t tlli = 0xffeeddbb;
+	gprs_rlcmac_dl_tbf *dl_tbf;
+	gprs_rlcmac_ul_tbf *ul_tbf;
+	GprsMs *ms;
+	static enum {UNKNOWN, IS_IDLE, IS_ACTIVE} last_cb = UNKNOWN;
+
+	struct MyCallback: public GprsMs::Callback {
+		virtual void ms_idle(class GprsMs *ms) {
+			OSMO_ASSERT(ms->is_idle());
+			printf("  ms_idle() was called\n");
+			last_cb = IS_IDLE;
+		}
+		virtual void ms_active(class GprsMs *ms) {
+			OSMO_ASSERT(!ms->is_idle());
+			printf("  ms_active() was called\n");
+			last_cb = IS_ACTIVE;
+		}
+	} cb;
+
+	printf("=== start %s ===\n", __func__);
+
+	ms = new GprsMs(tlli);
+	ms->set_callback(&cb);
+	ms->set_timeout(1);
+
+	OSMO_ASSERT(ms->is_idle());
+
+	dl_tbf = talloc_zero(tall_pcu_ctx, struct gprs_rlcmac_dl_tbf);
+	dl_tbf->direction = GPRS_RLCMAC_DL_TBF;
+	ul_tbf = talloc_zero(tall_pcu_ctx, struct gprs_rlcmac_ul_tbf);
+	ul_tbf->direction = GPRS_RLCMAC_UL_TBF;
+
+	OSMO_ASSERT(last_cb == UNKNOWN);
+
+	ms->attach_tbf(ul_tbf);
+	OSMO_ASSERT(!ms->is_idle());
+	OSMO_ASSERT(last_cb == IS_ACTIVE);
+
+	last_cb = UNKNOWN;
+
+	ms->attach_tbf(dl_tbf);
+	OSMO_ASSERT(!ms->is_idle());
+	OSMO_ASSERT(last_cb == UNKNOWN);
+
+	ms->detach_tbf(ul_tbf);
+	OSMO_ASSERT(!ms->is_idle());
+	OSMO_ASSERT(last_cb == UNKNOWN);
+
+	ms->detach_tbf(dl_tbf);
+	OSMO_ASSERT(!ms->is_idle());
+	OSMO_ASSERT(last_cb == UNKNOWN);
+
+	usleep(1100000);
+	osmo_timers_update();
+
+	OSMO_ASSERT(ms->is_idle());
+	OSMO_ASSERT(last_cb == IS_IDLE);
+
+	last_cb = UNKNOWN;
+	delete ms;
+	talloc_free(dl_tbf);
+	talloc_free(ul_tbf);
+
+	printf("=== end %s ===\n", __func__);
+}
+
 static const struct log_info_cat default_categories[] = {
 	{"DPCU", "", "GPRS Packet Control Unit (PCU)", LOGL_INFO, 1},
 };
@@ -437,6 +506,7 @@
 	test_ms_replace_tbf();
 	test_ms_change_tlli();
 	test_ms_storage();
+	test_ms_timeout();
 
 	if (getenv("TALLOC_REPORT_FULL"))
 		talloc_report_full(tall_pcu_ctx, stderr);
diff --git a/tests/ms/MsTest.err b/tests/ms/MsTest.err
index 86dbb5b..9e9df55 100644
--- a/tests/ms/MsTest.err
+++ b/tests/ms/MsTest.err
@@ -47,3 +47,10 @@
 Attaching TBF to MS object, TLLI = 0xffeeddbc, TBF = TBF(TFI=0 TLLI=0x00000000 DIR=UL STATE=NULL)
 Detaching TBF from MS object, TLLI = 0xffeeddbc, TBF = TBF(TFI=0 TLLI=0x00000000 DIR=UL STATE=NULL)
 Destroying MS object, TLLI = 0xffeeddbc
+Creating MS object, TLLI = 0xffeeddbb
+Attaching TBF to MS object, TLLI = 0xffeeddbb, TBF = TBF(TFI=0 TLLI=0x00000000 DIR=UL STATE=NULL)
+Attaching TBF to MS object, TLLI = 0xffeeddbb, TBF = TBF(TFI=0 TLLI=0x00000000 DIR=DL STATE=NULL)
+Detaching TBF from MS object, TLLI = 0xffeeddbb, TBF = TBF(TFI=0 TLLI=0x00000000 DIR=UL STATE=NULL)
+Detaching TBF from MS object, TLLI = 0xffeeddbb, TBF = TBF(TFI=0 TLLI=0x00000000 DIR=DL STATE=NULL)
+Timeout for MS object, TLLI = 0xffeeddbb
+Destroying MS object, TLLI = 0xffeeddbb
diff --git a/tests/ms/MsTest.ok b/tests/ms/MsTest.ok
index 004b36d..c49e840 100644
--- a/tests/ms/MsTest.ok
+++ b/tests/ms/MsTest.ok
@@ -12,3 +12,7 @@
 === end test_ms_change_tlli ===
 === start test_ms_storage ===
 === end test_ms_storage ===
+=== start test_ms_timeout ===
+  ms_active() was called
+  ms_idle() was called
+=== end test_ms_timeout ===