diff --git a/akka-bbb-apps/src/main/resources/application.conf b/akka-bbb-apps/src/main/resources/application.conf index 7f042e21e1..106bea5455 100755 --- a/akka-bbb-apps/src/main/resources/application.conf +++ b/akka-bbb-apps/src/main/resources/application.conf @@ -47,6 +47,12 @@ inactivity { timeLeft=300 } +expire { + # time in seconds + lastUserLeft = 60 + neverJoined = 300 +} + services { bbbWebAPI = "http://192.168.23.33/bigbluebutton/api" sharedSecret = "changeme" diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala index 4ceeaadec0..ea2b8ef501 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/SystemConfiguration.scala @@ -24,4 +24,7 @@ trait SystemConfiguration { lazy val inactivityDeadline = Try(config.getInt("inactivity.deadline")).getOrElse(2 * 3600) // 2 hours lazy val inactivityTimeLeft = Try(config.getInt("inactivity.timeLeft")).getOrElse(5 * 60) // 5 minutes + + lazy val expireLastUserLeft = Try(config.getInt("expire.lastUserLeft")).getOrElse(60) // 1 minute + lazy val expireNeverJoined = Try(config.getInt("expire.neverJoined")).getOrElse(5 * 60) // 5 minutes } diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala index 2bc622cd7b..e8c5dfdc3c 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/api/InMessages.scala @@ -40,6 +40,7 @@ case class DestroyMeeting(meetingID: String) extends InMessage case class StartMeeting(meetingID: String) extends InMessage case class EndMeeting(meetingId: String) extends InMessage case class LockSetting(meetingID: String, locked: Boolean, settings: Map[String, Boolean]) extends InMessage +case class UpdateMeetingExpireMonitor(meetingID: String, hasUser: Boolean) extends InMessage ////////////////////////////////////////////////////////////////////////////////////// // Breakout room diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala index ab4eda3a13..1a6197fc55 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActor.scala @@ -53,7 +53,7 @@ class MeetingActor(val mProps: MeetingProperties, def receive = { case msg: ActivityResponse => handleActivityResponse(msg) - case msg: MonitorNumberOfUsers => handleMonitorNumberOfWebUsers(msg) + case msg: MonitorNumberOfUsers => handleMonitorNumberOfUsers(msg) case msg: ValidateAuthToken => handleValidateAuthToken(msg) case msg: RegisterUser => handleRegisterUser(msg) case msg: UserJoinedVoiceConfMessage => handleUserJoinedVoiceConfMessage(msg) @@ -302,7 +302,12 @@ class MeetingActor(val mProps: MeetingProperties, } } - def handleMonitorNumberOfWebUsers(msg: MonitorNumberOfUsers) { + def handleMonitorNumberOfUsers(msg: MonitorNumberOfUsers) { + monitorNumberOfWebUsers() + monitorNumberOfUsers() + } + + def monitorNumberOfWebUsers() { if (Users.numWebUsers(liveMeeting.users) == 0 && liveMeeting.meetingModel.lastWebUserLeftOn > 0) { if (liveMeeting.timeNowInMinutes - liveMeeting.meetingModel.lastWebUserLeftOn > 2) { log.info("Empty meeting. Ejecting all users from voice. meetingId={}", mProps.meetingID) @@ -311,6 +316,12 @@ class MeetingActor(val mProps: MeetingProperties, } } + def monitorNumberOfUsers() { + val hasUsers = Users.numUsers(liveMeeting.users) != 0 + // TODO: We could use a better control over this message to send it just when it really matters :) + eventBus.publish(BigBlueButtonEvent(mProps.meetingID, UpdateMeetingExpireMonitor(mProps.meetingID, hasUsers))) + } + def handleSendTimeRemainingUpdate(msg: SendTimeRemainingUpdate) { if (mProps.duration > 0) { val endMeetingTime = liveMeeting.meetingModel.startedOn + (mProps.duration * 60) diff --git a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActorInternal.scala b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActorInternal.scala index 70e9927cc2..a698eba3e0 100755 --- a/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActorInternal.scala +++ b/akka-bbb-apps/src/main/scala/org/bigbluebutton/core/running/MeetingActorInternal.scala @@ -55,11 +55,32 @@ class MeetingActorInternal(val mProps: MeetingProperties, time } + private def getExpireNeverJoined(): Int = { + val time = expireNeverJoined + log.debug("ExpireNeverJoined: {} seconds", time) + time + } + + private def getExpireLastUserLeft(): Int = { + val time = expireLastUserLeft + log.debug("ExpireLastUserLeft: {} seconds", time) + time + } + + private val MonitorFrequency = 10 seconds + private val InactivityDeadline = FiniteDuration(getInactivityDeadline(), "seconds") private val InactivityTimeLeft = FiniteDuration(getInactivityTimeLeft(), "seconds") - private val MonitorFrequency = 10 seconds - private var deadline = InactivityDeadline.fromNow + private var inactivity = InactivityDeadline.fromNow private var inactivityWarning: Deadline = null + + private val ExpireMeetingDuration = FiniteDuration(mProps.duration, "minutes") + private val ExpireMeetingNeverJoined = FiniteDuration(getExpireNeverJoined(), "seconds") + private val ExpireMeetingLastUserLeft = FiniteDuration(getExpireLastUserLeft(), "seconds") + private var meetingExpire = ExpireMeetingNeverJoined.fromNow + // Zero minutes means the meeting has no duration control + private var meetingDuration: Deadline = if (ExpireMeetingDuration > (0 minutes)) ExpireMeetingDuration.fromNow else null + context.system.scheduler.schedule(5 seconds, MonitorFrequency, self, "Monitor") // Query to get voice conference users @@ -74,12 +95,14 @@ class MeetingActorInternal(val mProps: MeetingProperties, def receive = { case "Monitor" => handleMonitor() + case msg: UpdateMeetingExpireMonitor => handleUpdateMeetingExpireMonitor(msg) case msg: Object => handleMessage(msg) } def handleMonitor() { handleMonitorActivity() handleMonitorNumberOfWebUsers() + handleMonitorExpiration() } def handleMessage(msg: Object) { @@ -102,12 +125,12 @@ class MeetingActorInternal(val mProps: MeetingProperties, } private def handleMonitorActivity() { - if (deadline.isOverdue() && inactivityWarning != null && inactivityWarning.isOverdue()) { + if (inactivity.isOverdue() && inactivityWarning != null && inactivityWarning.isOverdue()) { log.info("Closing meeting {} due to inactivity for {} seconds", mProps.meetingID, InactivityDeadline.toSeconds) updateInactivityMonitors() eventBus.publish(BigBlueButtonEvent(mProps.meetingID, EndMeeting(mProps.meetingID))) // Or else make sure to send only one warning message - } else if (deadline.isOverdue() && inactivityWarning == null) { + } else if (inactivity.isOverdue() && inactivityWarning == null) { log.info("Sending inactivity warning to meeting {}", mProps.meetingID) outGW.send(new InactivityWarning(mProps.meetingID, InactivityTimeLeft.toSeconds)) // We add 5 seconds so clients will have enough time to process the message @@ -115,8 +138,38 @@ class MeetingActorInternal(val mProps: MeetingProperties, } } + private def handleMonitorExpiration() { + if (meetingExpire != null && meetingExpire.isOverdue()) { + // User related meeting expiration methods + log.debug("Meeting {} expired. No users", mProps.meetingID) + meetingExpire = null + eventBus.publish(BigBlueButtonEvent(mProps.meetingID, EndMeeting(mProps.meetingID))) + } else if (meetingDuration != null && meetingDuration.isOverdue()) { + // Default meeting duration + meetingDuration = null + log.debug("Meeting {} expired. Reached it's fixed duration of {}", mProps.meetingID, ExpireMeetingDuration.toString()) + eventBus.publish(BigBlueButtonEvent(mProps.meetingID, EndMeeting(mProps.meetingID))) + } + } + + private def handleUpdateMeetingExpireMonitor(msg: UpdateMeetingExpireMonitor) { + if (msg.hasUser) { + if (meetingExpire != null) { + // User joined. Forget about this expiration for now + log.debug("Meeting has users. Stopping expiration for meeting {}", mProps.meetingID) + meetingExpire = null + } + } else { + if (meetingExpire == null) { + // User list is empty. Start this meeting expiration method + log.debug("Meeting has no users. Starting {} expiration for meeting {}", ExpireMeetingLastUserLeft.toString(), mProps.meetingID) + meetingExpire = ExpireMeetingLastUserLeft.fromNow + } + } + } + private def updateInactivityMonitors() { - deadline = InactivityDeadline.fromNow + inactivity = InactivityDeadline.fromNow inactivityWarning = null } diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java index 40a0a14e49..d33e3907f5 100755 --- a/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/api/domain/Meeting.java @@ -27,8 +27,6 @@ import org.apache.commons.lang3.RandomStringUtils; public class Meeting { - private static final long MILLIS_IN_A_MINUTE = 60000; - private String name; private String extMeetingId; private String intMeetingId; @@ -65,8 +63,6 @@ public class Meeting { private final Boolean isBreakout; private final List breakoutRooms = new ArrayList(); - private long lastUserLeftOn = 0; - public Meeting(Builder builder) { name = builder.name; extMeetingId = builder.externalId; @@ -293,7 +289,6 @@ public class Meeting { public User userLeft(String userid){ User u = (User) users.remove(userid); - if (users.isEmpty()) lastUserLeftOn = System.currentTimeMillis(); return u; } @@ -317,54 +312,6 @@ public class Meeting { public String getDialNumber() { return dialNumber; } - - public boolean wasNeverJoined(int expiry) { - return (hasStarted() && !hasEnded() && nobodyJoined(expiry)); - } - - private boolean meetingInfinite() { - /* Meeting stays runs infinitely */ - return duration == 0; - } - - private boolean nobodyJoined(int expiry) { - if (expiry == 0) return false; /* Meeting stays created infinitely */ - - long now = System.currentTimeMillis(); - - return (!userHasJoined && (now - createdTime) > (expiry * MILLIS_IN_A_MINUTE)); - } - - private boolean hasBeenEmptyFor(int expiry) { - long now = System.currentTimeMillis(); - return (now - lastUserLeftOn > (expiry * MILLIS_IN_A_MINUTE)); - } - - private boolean isEmpty() { - return users.isEmpty(); - } - - public boolean hasExpired(int expiry) { - return (hasStarted() && userHasJoined && isEmpty() && hasBeenEmptyFor(expiry)); - } - - public boolean hasExceededDuration() { - return (hasStarted() && !hasEnded() && pastDuration()); - } - - private boolean pastDuration() { - if (meetingInfinite()) return false; - long now = System.currentTimeMillis(); - return (now - startTime > (duration * MILLIS_IN_A_MINUTE)); - } - - private boolean hasStarted() { - return startTime > 0; - } - - private boolean hasEnded() { - return endTime > 0; - } public int getNumListenOnly() { int sum = 0; diff --git a/bigbluebutton-config/bin/bbb-conf b/bigbluebutton-config/bin/bbb-conf index 1ef1013687..76dd01e5a1 100755 --- a/bigbluebutton-config/bin/bbb-conf +++ b/bigbluebutton-config/bin/bbb-conf @@ -1341,21 +1341,6 @@ check_state() { fi fi - if grep -q removeMeetingWhenEnded=false $SERVLET_DIR/bigbluebutton/WEB-INF/classes/bigbluebutton.properties; then - echo "# Warning: In" - echo "#" - echo "# $SERVLET_DIR/bigbluebutton/WEB-INF/classes/bigbluebutton.properties" - echo "#" - echo "# detected the setting" - echo "#" - echo "# removeMeetingWhenEnded=false" - echo "#" - echo "# You should set this value to true. It enables bbb-web to immediately purge a meeting from" - echo "# memory when receiving an end API call. Otherwise, users must wait about 2 minutes" - echo "# request before creating a meeting with the same meetingID but with different parameters." - echo - fi - if (( $MEM < 3940 )); then echo "# Warning: You are running BigBlueButton on a server with less than 4G of memory. Your" echo "# performance may suffer." diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties index f7b8dec5a4..078f95b46d 100755 --- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties +++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties @@ -115,19 +115,6 @@ defaultMaxUsers=0 # Current default is 0 (meeting doesn't end). defaultMeetingDuration=0 -# Remove the meeting from memory when the end API is called. -# This allows 3rd-party apps to recycle the meeting right-away -# instead of waiting for the meeting to expire (see below). -removeMeetingWhenEnded=true - -# The number of minutes before the system removes the meeting from memory. -defaultMeetingExpireDuration=1 - -# The number of minutes the system waits when a meeting is created and when -# a user joins. If after this period, a user hasn't joined, the meeting is -# removed from memory. -defaultMeetingCreateJoinDuration=5 - # Disable recording by default. # true - don't record even if record param in the api call is set to record # false - when record param is passed from api, override this default diff --git a/bigbluebutton-web/grails-app/conf/spring/resources.xml b/bigbluebutton-web/grails-app/conf/spring/resources.xml index 20edb6e04e..0ad5762aa0 100755 --- a/bigbluebutton-web/grails-app/conf/spring/resources.xml +++ b/bigbluebutton-web/grails-app/conf/spring/resources.xml @@ -31,8 +31,6 @@ with BigBlueButton; if not, see . - - . - - - - diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/MeetingService.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/MeetingService.java index 7d94c47193..8030753a95 100755 --- a/bigbluebutton-web/src/java/org/bigbluebutton/api/MeetingService.java +++ b/bigbluebutton-web/src/java/org/bigbluebutton/api/MeetingService.java @@ -51,7 +51,6 @@ import org.bigbluebutton.api.messaging.messages.MeetingDestroyed; import org.bigbluebutton.api.messaging.messages.MeetingEnded; import org.bigbluebutton.api.messaging.messages.MeetingStarted; import org.bigbluebutton.api.messaging.messages.RegisterUser; -import org.bigbluebutton.api.messaging.messages.RemoveExpiredMeetings; import org.bigbluebutton.api.messaging.messages.UserJoined; import org.bigbluebutton.api.messaging.messages.UserJoinedVoice; import org.bigbluebutton.api.messaging.messages.UserLeft; @@ -63,7 +62,6 @@ import org.bigbluebutton.api.messaging.messages.UserStatusChanged; import org.bigbluebutton.api.messaging.messages.UserUnsharedWebcam; import org.bigbluebutton.presentation.PresentationUrlDownloadService; import org.bigbluebutton.api.messaging.messages.StunTurnInfoRequested; -import org.bigbluebutton.web.services.ExpiredMeetingCleanupTimerTask; import org.bigbluebutton.web.services.RegisteredUserCleanupTimerTask; import org.bigbluebutton.web.services.turn.StunServer; import org.bigbluebutton.web.services.turn.StunTurnService; @@ -90,14 +88,10 @@ public class MeetingService implements MessageListener { private final ConcurrentMap meetings; private final ConcurrentMap sessions; - private int defaultMeetingExpireDuration = 1; - private int defaultMeetingCreateJoinDuration = 5; private RecordingService recordingService; private MessagingService messagingService; - private ExpiredMeetingCleanupTimerTask cleaner; private RegisteredUserCleanupTimerTask registeredUserCleaner; private StunTurnService stunTurnService; - private boolean removeMeetingWhenEnded = false; private ParamsProcessorUtil paramsProcessorUtil; private PresentationUrlDownloadService presDownloadService; @@ -131,13 +125,6 @@ public class MeetingService implements MessageListener { return user; } - /** - * Remove the meetings that have ended from the list of running meetings. - */ - public void removeExpiredMeetings() { - handle(new RemoveExpiredMeetings()); - } - /** * Remove registered users who did not successfully joined the meeting. */ @@ -162,7 +149,6 @@ public class MeetingService implements MessageListener { } } } - handle(new RemoveExpiredMeetings()); } private void kickOffProcessingOfRecording(Meeting m) { @@ -203,77 +189,6 @@ public class MeetingService implements MessageListener { } } - private void checkAndRemoveExpiredMeetings() { - for (Meeting m : meetings.values()) { - if (m.hasExpired(defaultMeetingExpireDuration)) { - Map logData = new HashMap(); - logData.put("meetingId", m.getInternalId()); - logData.put("externalMeetingId", m.getExternalId()); - logData.put("name", m.getName()); - logData.put("event", "removing_meeting"); - logData.put("description", "Meeting has expired."); - - Gson gson = new Gson(); - String logStr = gson.toJson(logData); - log.info("Removing expired meeting: data={}", logStr); - - processMeetingForRemoval(m); - continue; - } - - if (m.isForciblyEnded()) { - Map logData = new HashMap(); - logData.put("meetingId", m.getInternalId()); - logData.put("externalMeetingId", m.getExternalId()); - logData.put("name", m.getName()); - logData.put("event", "removing_meeting"); - logData.put("description", "Meeting forcefully ended."); - - Gson gson = new Gson(); - String logStr = gson.toJson(logData); - - log.info("Removing ended meeting: data={}", logStr); - processMeetingForRemoval(m); - continue; - } - - if (m.wasNeverJoined(defaultMeetingCreateJoinDuration)) { - Map logData = new HashMap(); - logData.put("meetingId", m.getInternalId()); - logData.put("externalMeetingId", m.getExternalId()); - logData.put("name", m.getName()); - logData.put("event", "removing_meeting"); - logData.put("description", "Meeting has not been joined."); - - Gson gson = new Gson(); - String logStr = gson.toJson(logData); - - log.info("Removing un-joined meeting: data={}", logStr); - - destroyMeeting(m.getInternalId()); - meetings.remove(m.getInternalId()); - removeUserSessions(m.getInternalId()); - continue; - } - - if (m.hasExceededDuration()) { - Map logData = new HashMap(); - logData.put("meetingId", m.getInternalId()); - logData.put("externalMeetingId", m.getExternalId()); - logData.put("name", m.getName()); - logData.put("event", "removing_meeting"); - logData.put("description", "Meeting exceeded duration."); - - Gson gson = new Gson(); - String logStr = gson.toJson(logData); - - log.info("Removing past duration meeting: data={}", logStr); - - endMeeting(m.getInternalId()); - } - } - } - private void destroyMeeting(String meetingID) { messagingService.destroyMeeting(meetingID); } @@ -608,12 +523,10 @@ public class MeetingService implements MessageListener { Meeting m = getMeeting(message.meetingId); if (m != null) { m.setForciblyEnded(true); - if (removeMeetingWhenEnded) { - processRecording(m.getInternalId()); - destroyMeeting(m.getInternalId()); - meetings.remove(m.getInternalId()); - removeUserSessions(m.getInternalId()); - } + processRecording(m.getInternalId()); + destroyMeeting(m.getInternalId()); + meetings.remove(m.getInternalId()); + removeUserSessions(m.getInternalId()); } } @@ -947,8 +860,6 @@ public class MeetingService implements MessageListener { userSharedWebcam((UserSharedWebcam) message); } else if (message instanceof UserUnsharedWebcam) { userUnsharedWebcam((UserUnsharedWebcam) message); - } else if (message instanceof RemoveExpiredMeetings) { - checkAndRemoveExpiredMeetings(); } else if (message instanceof CreateMeeting) { processCreateMeeting((CreateMeeting) message); } else if (message instanceof EndMeeting) { @@ -997,18 +908,9 @@ public class MeetingService implements MessageListener { public void stop() { processMessage = false; - cleaner.stop(); registeredUserCleaner.stop(); } - public void setDefaultMeetingCreateJoinDuration(int expiration) { - this.defaultMeetingCreateJoinDuration = expiration; - } - - public void setDefaultMeetingExpireDuration(int meetingExpiration) { - this.defaultMeetingExpireDuration = meetingExpiration; - } - public void setRecordingService(RecordingService s) { recordingService = s; } @@ -1017,17 +919,6 @@ public class MeetingService implements MessageListener { messagingService = mess; } - public void setExpiredMeetingCleanupTimerTask( - ExpiredMeetingCleanupTimerTask c) { - cleaner = c; - cleaner.setMeetingService(this); - cleaner.start(); - } - - public void setRemoveMeetingWhenEnded(boolean s) { - removeMeetingWhenEnded = s; - } - public void setRegisteredUserCleanupTimerTask( RegisteredUserCleanupTimerTask c) { registeredUserCleaner = c; diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/RemoveExpiredMeetings.java b/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/RemoveExpiredMeetings.java deleted file mode 100755 index 6282e017ed..0000000000 --- a/bigbluebutton-web/src/java/org/bigbluebutton/api/messaging/messages/RemoveExpiredMeetings.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.bigbluebutton.api.messaging.messages; - -public class RemoveExpiredMeetings implements IMessage { - -} diff --git a/bigbluebutton-web/src/java/org/bigbluebutton/web/services/ExpiredMeetingCleanupTimerTask.java b/bigbluebutton-web/src/java/org/bigbluebutton/web/services/ExpiredMeetingCleanupTimerTask.java deleted file mode 100644 index db8b5b8b2c..0000000000 --- a/bigbluebutton-web/src/java/org/bigbluebutton/web/services/ExpiredMeetingCleanupTimerTask.java +++ /dev/null @@ -1,55 +0,0 @@ -/** -* BigBlueButton open source conferencing system - http://www.bigbluebutton.org/ -* -* Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below). -* -* This program is free software; you can redistribute it and/or modify it under the -* terms of the GNU Lesser General Public License as published by the Free Software -* Foundation; either version 3.0 of the License, or (at your option) any later -* version. -* -* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY -* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A -* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public License along -* with BigBlueButton; if not, see . -* -*/ - -package org.bigbluebutton.web.services; - -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -import org.bigbluebutton.api.MeetingService; - -public class ExpiredMeetingCleanupTimerTask { - - private MeetingService service; - private ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(1); - private long runEvery = 60000; - - public void setMeetingService(MeetingService svc) { - this.service = svc; - } - - public void start() { - scheduledThreadPool.scheduleWithFixedDelay(new CleanupTask(), 60000, runEvery, TimeUnit.MILLISECONDS); - } - - public void stop() { - scheduledThreadPool.shutdownNow(); - } - - public void setRunEvery(long v) { - runEvery = v; - } - - private class CleanupTask implements Runnable { - public void run() { - service.removeExpiredMeetings(); - } - } -} \ No newline at end of file