diff --git a/app/loader/SignalConsoApplicationLoader.scala b/app/loader/SignalConsoApplicationLoader.scala index 2e6eb6f0..557fd72a 100644 --- a/app/loader/SignalConsoApplicationLoader.scala +++ b/app/loader/SignalConsoApplicationLoader.scala @@ -82,7 +82,7 @@ import repositories.socialnetwork.SocialNetworkRepository import repositories.socialnetwork.SocialNetworkRepositoryInterface import repositories.subscription.SubscriptionRepository import repositories.subscription.SubscriptionRepositoryInterface -import repositories.tasklock.TaskLockRepository +import repositories.tasklock.TaskRepository import repositories.user.UserRepository import repositories.user.UserRepositoryInterface import repositories.usersettings.UserReportsFiltersRepository @@ -180,7 +180,7 @@ class SignalConsoComponents( val dbConfig: DatabaseConfig[JdbcProfile] = slickApi.dbConfig[JdbcProfile](DbName("default")) - val taskLockRepository = new TaskLockRepository(dbConfig) + val taskRepository = new TaskRepository(dbConfig) val blacklistedEmailsRepository: BlacklistedEmailsRepositoryInterface = new BlacklistedEmailsRepository(dbConfig) val companyAccessRepository: CompanyAccessRepositoryInterface = new CompanyAccessRepository(dbConfig) val accessTokenRepository: AccessTokenRepositoryInterface = @@ -444,7 +444,7 @@ class SignalConsoComponents( companyRepository, mailService, taskConfiguration, - taskLockRepository, + taskRepository, messagesApi ) reportClosureTask.schedule() @@ -456,7 +456,7 @@ class SignalConsoComponents( mailService, companiesVisibilityOrchestrator, taskConfiguration, - taskLockRepository + taskRepository ) reportReminderTask.schedule() @@ -471,7 +471,7 @@ class SignalConsoComponents( companySyncService, companySyncRepository, taskConfiguration, - taskLockRepository + taskRepository ) companyUpdateTask.schedule() @@ -485,7 +485,7 @@ class SignalConsoComponents( userRepository, mailService, taskConfiguration, - taskLockRepository + taskRepository ) reportNotificationTask.schedule() @@ -503,7 +503,7 @@ class SignalConsoComponents( inactiveDgccrfAccountRemoveTask, inactiveDgccrfAccountReminderTask, applicationConfiguration.task, - taskLockRepository + taskRepository ) inactiveAccountTask.schedule() diff --git a/app/repositories/tasklock/TaskDetails.scala b/app/repositories/tasklock/TaskDetails.scala new file mode 100644 index 00000000..e2d673f0 --- /dev/null +++ b/app/repositories/tasklock/TaskDetails.scala @@ -0,0 +1,31 @@ +package repositories.tasklock + +import play.api.libs.json.Format +import play.api.libs.json.JsResult +import play.api.libs.json.JsValue +import play.api.libs.json.Json +import play.api.libs.json.OWrites +import play.api.libs.json.Reads._ +import play.api.libs.json.Writes._ +import java.time.LocalTime +import java.time.OffsetDateTime +import scala.concurrent.duration._ + +case class TaskDetails( + id: Int, + name: String, + startTime: LocalTime, + interval: FiniteDuration, + lastRunDate: OffsetDateTime, + lastRunStatus: String +) + +object TaskDetails { + implicit private val finiteDurationFormat: Format[FiniteDuration] = new Format[FiniteDuration] { + def reads(json: JsValue): JsResult[FiniteDuration] = LongReads.reads(json).map(_.milliseconds) + + def writes(o: FiniteDuration): JsValue = LongWrites.writes(o.toMillis) + } + + implicit val writes: OWrites[TaskDetails] = Json.writes[TaskDetails] +} diff --git a/app/repositories/tasklock/TaskDetailsTable.scala b/app/repositories/tasklock/TaskDetailsTable.scala new file mode 100644 index 00000000..222a9b7e --- /dev/null +++ b/app/repositories/tasklock/TaskDetailsTable.scala @@ -0,0 +1,39 @@ +package repositories.tasklock + +import repositories.PostgresProfile.api._ +import repositories.TypedDatabaseTable +import slick.lifted.ProvenShape + +import java.time.Duration +import java.time.LocalTime +import java.time.OffsetDateTime +import scala.jdk.DurationConverters._ + +class TaskDetailsTable(tag: Tag) extends TypedDatabaseTable[TaskDetails, Int](tag, "task_details") { + + def name = column[String]("name") + def startTime = column[LocalTime]("start_time") + def interval = column[Duration]("interval") + def lastRunDate = column[OffsetDateTime]("last_run_date") + def lastRunStatus = column[String]("last_run_status") + + type TaskData = (Int, String, LocalTime, Duration, OffsetDateTime, String) + + def constructTaskDetails: TaskData => TaskDetails = { + case (id, name, startTime, interval, lastRunDate, lastRunStatus) => + TaskDetails(id, name, startTime, interval.toScala, lastRunDate, lastRunStatus) + } + + def extractTaskDetails: PartialFunction[TaskDetails, TaskData] = { + case TaskDetails(id, name, startTime, interval, lastRunDate, lastRunStatus) => + (id, name, startTime, interval.toJava, lastRunDate, lastRunStatus) + } + + override def * : ProvenShape[TaskDetails] = + (id, name, startTime, interval, lastRunDate, lastRunStatus) <> (constructTaskDetails, extractTaskDetails.lift) + +} + +object TaskDetailsTable { + val table = TableQuery[TaskDetailsTable] +} diff --git a/app/repositories/tasklock/TaskLockRepository.scala b/app/repositories/tasklock/TaskLockRepository.scala deleted file mode 100644 index 6598b2fe..00000000 --- a/app/repositories/tasklock/TaskLockRepository.scala +++ /dev/null @@ -1,26 +0,0 @@ -package repositories.tasklock - -import slick.basic.DatabaseConfig -import slick.jdbc.JdbcProfile -import repositories.PostgresProfile.api._ - -import scala.concurrent.Future - -trait TaskLockRepositoryInterface { - def acquire(id: Int): Future[Boolean] - def release(id: Int): Future[Boolean] -} - -class TaskLockRepository(dbConfig: DatabaseConfig[JdbcProfile]) extends TaskLockRepositoryInterface { - import dbConfig._ - - def acquire(id: Int): Future[Boolean] = - db.run( - sql"""select pg_try_advisory_lock($id)""".as[Boolean].head - ) - - def release(id: Int): Future[Boolean] = - db.run( - sql"""select pg_advisory_unlock($id)""".as[Boolean].head - ) -} diff --git a/app/repositories/tasklock/TaskRepository.scala b/app/repositories/tasklock/TaskRepository.scala new file mode 100644 index 00000000..a738ed4b --- /dev/null +++ b/app/repositories/tasklock/TaskRepository.scala @@ -0,0 +1,34 @@ +package repositories.tasklock + +import slick.basic.DatabaseConfig +import slick.jdbc.JdbcProfile +import repositories.PostgresProfile.api._ +import repositories.TypedCRUDRepository +import repositories.TypedCRUDRepositoryInterface + +import scala.concurrent.ExecutionContext +import scala.concurrent.Future + +trait TaskRepositoryInterface extends TypedCRUDRepositoryInterface[TaskDetails, Int] { + def acquireLock(id: Int): Future[Boolean] + def releaseLock(id: Int): Future[Boolean] +} + +class TaskRepository(override val dbConfig: DatabaseConfig[JdbcProfile])(implicit override val ec: ExecutionContext) + extends TypedCRUDRepository[TaskDetailsTable, TaskDetails, Int] + with TaskRepositoryInterface { + + import dbConfig._ + + override val table = TaskDetailsTable.table + + def acquireLock(id: Int): Future[Boolean] = + db.run( + sql"""select pg_try_advisory_lock($id)""".as[Boolean].head + ) + + def releaseLock(id: Int): Future[Boolean] = + db.run( + sql"""select pg_advisory_unlock($id)""".as[Boolean].head + ) +} diff --git a/app/repositories/website/WebsiteRepository.scala b/app/repositories/website/WebsiteRepository.scala index 464a4483..6b1a3b00 100644 --- a/app/repositories/website/WebsiteRepository.scala +++ b/app/repositories/website/WebsiteRepository.scala @@ -92,16 +92,18 @@ class WebsiteRepository( ) def deprecatedSearchCompaniesByHost(host: String): Future[Seq[(Website, Company)]] = - URL(host).getHost.map { h => - db.run( - table - .filter(_.host === h) - .filter(_.identificationStatus inSet List(IdentificationStatus.Identified)) - .join(CompanyTable.table) - .on(_.companyId === _.id) - .result - ) - } getOrElse (Future(Nil)) + URL(host).getHost + .map { h => + db.run( + table + .filter(_.host === h) + .filter(_.identificationStatus inSet List(IdentificationStatus.Identified)) + .join(CompanyTable.table) + .on(_.companyId === _.id) + .result + ) + } + .getOrElse(Future(Nil)) override def removeOtherNonIdentifiedWebsitesWithSameHost(website: Website): Future[Int] = db.run( @@ -115,7 +117,7 @@ class WebsiteRepository( override def searchCompaniesByUrl( url: String ): Future[Seq[(Website, Company)]] = - URL(url).getHost.map(searchCompaniesByHost(_)).getOrElse(Future(Nil)) + URL(url).getHost.map(searchCompaniesByHost).getOrElse(Future(Nil)) override def listWebsitesCompaniesByReportCount( maybeHost: Option[String], diff --git a/app/tasks/ScheduledTask.scala b/app/tasks/ScheduledTask.scala index 10c60e3d..2f49fc1c 100644 --- a/app/tasks/ScheduledTask.scala +++ b/app/tasks/ScheduledTask.scala @@ -3,10 +3,12 @@ package tasks import akka.actor.ActorSystem import config.TaskConfiguration import play.api.Logger -import repositories.tasklock.TaskLockRepositoryInterface +import repositories.tasklock.TaskRepositoryInterface +import repositories.tasklock.TaskDetails import utils.Logs.RichLogger import java.time.LocalTime +import java.time.OffsetDateTime import scala.concurrent.duration.DurationInt import scala.concurrent.duration.FiniteDuration import scala.concurrent.ExecutionContext @@ -17,7 +19,7 @@ import scala.util.Success abstract class ScheduledTask( taskId: Int, taskName: String, - taskLockRepository: TaskLockRepositoryInterface, + taskRepository: TaskRepositoryInterface, actorSystem: ActorSystem, taskConfiguration: TaskConfiguration )(implicit ec: ExecutionContext) { @@ -26,17 +28,32 @@ abstract class ScheduledTask( val startTime: LocalTime val interval: FiniteDuration + private def createTaskModel(status: String) = + TaskDetails(taskId, taskName, startTime, interval, lastRunDate = OffsetDateTime.now(), status) + + private def createOrUpdateTaskDetails(taskModel: TaskDetails) = + taskRepository + .createOrUpdate(taskModel) + .map(_ => logger.info(s"$taskName updated in DB")) + .recover(err => logger.errorWithTitle("task_failed", s"$taskName failed", err)) + def runTask(): Future[Unit] private def runTaskWithLock(): Unit = (for { - lockAcquired <- taskLockRepository.acquire(taskId) + lockAcquired <- taskRepository.acquireLock(taskId) _ <- if (lockAcquired) { logger.info(s"Lock acquired for $taskName.") runTask() - .map(_ => logger.info(s"$taskName finished")) - .recover(err => logger.errorWithTitle("task_failed", s"$taskName failed", err)) + .flatMap { _ => + logger.info(s"$taskName finished") + createOrUpdateTaskDetails(createTaskModel("success")) + } + .recoverWith { err => + logger.errorWithTitle("task_failed", s"$taskName failed", err) + createOrUpdateTaskDetails(createTaskModel("failure")) + } } else { logger.info(s"Lock for $taskName is already taken by another instance. Nothing to do here.") Future.unit @@ -46,7 +63,7 @@ abstract class ScheduledTask( private def release(): Unit = actorSystem.scheduler.scheduleOnce(1.minute) { logger.debug(s"Releasing lock for $taskName with id $taskId") - taskLockRepository.release(taskId).onComplete { + taskRepository.releaseLock(taskId).onComplete { case Success(_) => logger.debug(s"Lock released for $taskName with id $taskId") case Failure(err) => diff --git a/app/tasks/account/InactiveAccountTask.scala b/app/tasks/account/InactiveAccountTask.scala index 1d9715f1..f09c3d76 100644 --- a/app/tasks/account/InactiveAccountTask.scala +++ b/app/tasks/account/InactiveAccountTask.scala @@ -3,7 +3,7 @@ package tasks.account import akka.actor.ActorSystem import config.TaskConfiguration import play.api.Logger -import repositories.tasklock.TaskLockRepositoryInterface +import repositories.tasklock.TaskRepositoryInterface import tasks.ScheduledTask import java.time.LocalTime @@ -19,7 +19,7 @@ class InactiveAccountTask( inactiveDgccrfAccountRemoveTask: InactiveDgccrfAccountRemoveTask, inactiveDgccrfAccountSendReminderTask: InactiveDgccrfAccountReminderTask, taskConfiguration: TaskConfiguration, - taskLockRepository: TaskLockRepositoryInterface + taskLockRepository: TaskRepositoryInterface )(implicit executionContext: ExecutionContext) extends ScheduledTask(1, "inactive_account_task", taskLockRepository, actorSystem, taskConfiguration) { diff --git a/app/tasks/company/CompanyUpdateTask.scala b/app/tasks/company/CompanyUpdateTask.scala index 3ea1799b..9ff39a12 100644 --- a/app/tasks/company/CompanyUpdateTask.scala +++ b/app/tasks/company/CompanyUpdateTask.scala @@ -12,7 +12,7 @@ import play.api.Logger import repositories.company.CompanyRepositoryInterface import repositories.company.CompanySyncRepositoryInterface import repositories.company.CompanyTable -import repositories.tasklock.TaskLockRepositoryInterface +import repositories.tasklock.TaskRepositoryInterface import tasks.ScheduledTask import java.time.LocalTime @@ -29,7 +29,7 @@ class CompanyUpdateTask( companySyncService: CompanySyncServiceInterface, companySyncRepository: CompanySyncRepositoryInterface, taskConfiguration: TaskConfiguration, - taskLockRepository: TaskLockRepositoryInterface + taskLockRepository: TaskRepositoryInterface )(implicit executionContext: ExecutionContext, materializer: Materializer diff --git a/app/tasks/report/ReportClosureTask.scala b/app/tasks/report/ReportClosureTask.scala index 24d5a580..d472e32d 100644 --- a/app/tasks/report/ReportClosureTask.scala +++ b/app/tasks/report/ReportClosureTask.scala @@ -12,7 +12,7 @@ import play.api.i18n.MessagesApi import repositories.company.CompanyRepositoryInterface import repositories.event.EventRepositoryInterface import repositories.report.ReportRepositoryInterface -import repositories.tasklock.TaskLockRepositoryInterface +import repositories.tasklock.TaskRepositoryInterface import services.Email.ConsumerReportClosedNoAction import services.Email.ConsumerReportClosedNoReading import services.ConsumerEmail @@ -42,7 +42,7 @@ class ReportClosureTask( companyRepository: CompanyRepositoryInterface, mailService: MailService, taskConfiguration: TaskConfiguration, - taskLockRepository: TaskLockRepositoryInterface, + taskLockRepository: TaskRepositoryInterface, messagesApi: MessagesApi )(implicit val executionContext: ExecutionContext) extends ScheduledTask(2, "report_closure_task", taskLockRepository, actorSystem, taskConfiguration) { diff --git a/app/tasks/report/ReportNotificationTask.scala b/app/tasks/report/ReportNotificationTask.scala index af5d022f..a5af1a62 100644 --- a/app/tasks/report/ReportNotificationTask.scala +++ b/app/tasks/report/ReportNotificationTask.scala @@ -13,7 +13,7 @@ import models.report.ReportFilter import play.api.Logger import repositories.report.ReportRepositoryInterface import repositories.subscription.SubscriptionRepositoryInterface -import repositories.tasklock.TaskLockRepositoryInterface +import repositories.tasklock.TaskRepositoryInterface import repositories.user.UserRepositoryInterface import services.Email.DgccrfReportNotification import services.MailService @@ -38,7 +38,7 @@ class ReportNotificationTask( userRepository: UserRepositoryInterface, mailService: MailService, taskConfiguration: TaskConfiguration, - taskLockRepository: TaskLockRepositoryInterface + taskLockRepository: TaskRepositoryInterface )(implicit executionContext: ExecutionContext) extends ScheduledTask(3, "report_notification_task", taskLockRepository, actorSystem, taskConfiguration) { diff --git a/app/tasks/report/ReportRemindersTask.scala b/app/tasks/report/ReportRemindersTask.scala index 09277c43..4a00b629 100644 --- a/app/tasks/report/ReportRemindersTask.scala +++ b/app/tasks/report/ReportRemindersTask.scala @@ -11,7 +11,7 @@ import orchestrators.CompaniesVisibilityOrchestrator import play.api.Logger import repositories.event.EventRepositoryInterface import repositories.report.ReportRepositoryInterface -import repositories.tasklock.TaskLockRepositoryInterface +import repositories.tasklock.TaskRepositoryInterface import services.Email.ProReportsReadReminder import services.Email.ProReportsUnreadReminder import services.Email @@ -38,7 +38,7 @@ class ReportRemindersTask( mailService: MailServiceInterface, companiesVisibilityOrchestrator: CompaniesVisibilityOrchestrator, taskConfiguration: TaskConfiguration, - taskLockRepository: TaskLockRepositoryInterface + taskLockRepository: TaskRepositoryInterface )(implicit val executionContext: ExecutionContext) extends ScheduledTask(4, "report_reminders_task", taskLockRepository, actorSystem, taskConfiguration) { diff --git a/conf/db/migration/default/V17__task_details_table.sql b/conf/db/migration/default/V17__task_details_table.sql new file mode 100644 index 00000000..f50c9f60 --- /dev/null +++ b/conf/db/migration/default/V17__task_details_table.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS task_details ( + id UUID NOT NULL PRIMARY KEY, + name VARCHAR NOT NULL, + start_time TIMESTAMP WITH TIME ZONE NOT NULL, + interval BIGINT NOT NULL, + last_run_date TIMESTAMP WITH TIME ZONE NOT NULL, + last_run_status VARCHAR NOT NULL +); \ No newline at end of file diff --git a/conf/routes b/conf/routes index 125e8ed5..946d94e6 100644 --- a/conf/routes +++ b/conf/routes @@ -74,7 +74,7 @@ POST /api/admin/test-pdf controll POST /api/admin/emails/reportAckToConsumer controllers.AdminController.sendReportAckToConsumer() POST /api/admin/emails/proAckToConsumer controllers.AdminController.sendProAckToConsumer() POST /api/admin/emails/newReportToPro controllers.AdminController.sendNewReportToPro() - POST /api/admin/emails/resend controllers.AdminController.resend(start: OffsetDateTime, end: OffsetDateTime, emailType: models.ResendEmailType) +POST /api/admin/emails/resend controllers.AdminController.resend(start: OffsetDateTime, end: OffsetDateTime, emailType: models.ResendEmailType) # Async files API GET /api/async-files controllers.AsyncFileController.listAsyncFiles(kind: Option[String]) diff --git a/test/tasks/account/InactiveAccountTaskSpec.scala b/test/tasks/account/InactiveAccountTaskSpec.scala index 5697350a..ac886c11 100644 --- a/test/tasks/account/InactiveAccountTaskSpec.scala +++ b/test/tasks/account/InactiveAccountTaskSpec.scala @@ -9,9 +9,10 @@ import org.specs2.concurrent.ExecutionEnv import org.specs2.matcher.FutureMatchers import play.api.mvc.Results import play.api.test.WithApplication -import repositories.tasklock.TaskLockRepositoryInterface +import repositories.tasklock.TaskRepositoryInterface import utils.AppSpec import utils.Fixtures +import utils.TaskRepositoryMock import utils.TestApp import java.time.LocalDateTime @@ -21,7 +22,6 @@ import java.time.Period import java.time.ZoneOffset import java.time.temporal.ChronoUnit import scala.concurrent.Await -import scala.concurrent.Future import scala.concurrent.duration.Duration class InactiveAccountTaskSpec(implicit ee: ExecutionEnv) @@ -33,11 +33,7 @@ class InactiveAccountTaskSpec(implicit ee: ExecutionEnv) val (app, components) = TestApp.buildApp( ) - val lockRepositoryMock: TaskLockRepositoryInterface = new TaskLockRepositoryInterface { - override def acquire(id: Int): Future[Boolean] = Future.successful(true) - - override def release(id: Int): Future[Boolean] = Future.successful(true) - } + val taskRepositoryMock: TaskRepositoryInterface = new TaskRepositoryMock() lazy val userRepository = components.userRepository lazy val asyncFileRepository = components.asyncFileRepository @@ -123,7 +119,7 @@ class InactiveAccountTaskSpec(implicit ee: ExecutionEnv) inactiveDgccrfAccountRemoveTask, inactiveDgccrfAccountReminderTask, conf, - lockRepositoryMock + taskRepositoryMock ) .runTask(now.atOffset(ZoneOffset.UTC)) userList <- userRepository.list() diff --git a/test/tasks/company/CompanyUpdateTaskSpec.scala b/test/tasks/company/CompanyUpdateTaskSpec.scala index 4a3ae92a..21abb118 100644 --- a/test/tasks/company/CompanyUpdateTaskSpec.scala +++ b/test/tasks/company/CompanyUpdateTaskSpec.scala @@ -7,9 +7,9 @@ import org.specs2.concurrent.ExecutionEnv import org.specs2.matcher.FutureMatchers import org.specs2.mock.Mockito import org.specs2.mutable -import repositories.tasklock.TaskLockRepositoryInterface import utils.AppSpec import utils.Fixtures +import utils.TaskRepositoryMock import utils.TestApp import java.time.OffsetDateTime @@ -26,11 +26,7 @@ class CompanyUpdateTaskSpec(implicit ee: ExecutionEnv) val (app, components) = TestApp.buildApp() implicit val mat: Materializer = app.materializer - val taskLockRepositoryMock = new TaskLockRepositoryInterface { - override def acquire(id: Int): Future[Boolean] = Future.successful(true) - - override def release(id: Int): Future[Boolean] = Future.successful(true) - } + val taskLockRepositoryMock = new TaskRepositoryMock() "CompanyUpdateTask" should { sequential diff --git a/test/tasks/report/ReportRemindersTaskUnitSpec.scala b/test/tasks/report/ReportRemindersTaskUnitSpec.scala index b468ab3d..82c1f2e7 100644 --- a/test/tasks/report/ReportRemindersTaskUnitSpec.scala +++ b/test/tasks/report/ReportRemindersTaskUnitSpec.scala @@ -18,7 +18,6 @@ import repositories.company.CompanyRepositoryInterface import repositories.companyaccess.CompanyAccessRepositoryInterface import repositories.event.EventRepositoryInterface import repositories.report.ReportRepositoryInterface -import repositories.tasklock.TaskLockRepositoryInterface import services.Email import services.MailServiceInterface import utils.Constants.ActionEvent.EMAIL_PRO_NEW_REPORT @@ -26,6 +25,7 @@ import utils.Constants.ActionEvent.EMAIL_PRO_REMIND_NO_READING import utils.Constants.EventType import utils.Fixtures import utils.SIREN +import utils.TaskRepositoryMock import java.time.LocalTime import java.time.OffsetDateTime @@ -37,11 +37,7 @@ import scala.concurrent.duration.DurationInt class ReportRemindersTaskUnitSpec extends Specification with FutureMatchers { - val taskLockRepositoryMock = new TaskLockRepositoryInterface { - override def acquire(id: Int): Future[Boolean] = Future.successful(true) - - override def release(id: Int): Future[Boolean] = Future.successful(true) - } + val taskLockRepositoryMock = new TaskRepositoryMock() val taskConf = TaskConfiguration( active = true, diff --git a/test/utils/TaskRepositoryMock.scala b/test/utils/TaskRepositoryMock.scala new file mode 100644 index 00000000..e8f3524c --- /dev/null +++ b/test/utils/TaskRepositoryMock.scala @@ -0,0 +1,25 @@ +package utils + +import repositories.tasklock.TaskDetails +import repositories.tasklock.TaskRepositoryInterface + +import scala.concurrent.Future + +class TaskRepositoryMock extends TaskRepositoryInterface { + + override def acquireLock(id: Int): Future[Boolean] = Future.successful(true) + + override def releaseLock(id: Int): Future[Boolean] = Future.successful(true) + + override def create(element: TaskDetails): Future[TaskDetails] = ??? + + override def update(id: Int, element: TaskDetails): Future[TaskDetails] = ??? + + override def createOrUpdate(element: TaskDetails): Future[TaskDetails] = Future.successful(element) + + override def get(id: Int): Future[Option[TaskDetails]] = ??? + + override def delete(id: Int): Future[Int] = ??? + + override def list(): Future[List[TaskDetails]] = ??? +}