blob: 725eef79ff743a1db3f7f2043a6bd8ecf3e5c891 [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright 2020 - The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""LocalInstanceLock class."""
import errno
import fcntl
import logging
import os
from acloud import errors
from acloud.internal.lib import utils
logger = logging.getLogger(__name__)
_LOCK_FILE_SIZE = 1
# An empty file is equivalent to NOT_IN_USE.
_IN_USE_STATE = b"I"
_NOT_IN_USE_STATE = b"N"
_DEFAULT_TIMEOUT_SECS = 5
class LocalInstanceLock:
"""The class that controls a lock file for a local instance.
Acloud acquires the lock file of a local instance before it creates,
deletes, or queries it. The lock prevents multiple acloud processes from
accessing an instance simultaneously.
The lock file records whether the instance is in use. Acloud checks the
state when it needs an unused id to create a new instance.
Attributes:
_file_path: The path to the lock file.
_file_desc: The file descriptor of the file. It is set to None when
this object does not hold the lock.
"""
def __init__(self, file_path):
self._file_path = file_path
self._file_desc = None
def _Flock(self, timeout_secs):
"""Call fcntl.flock with timeout.
Args:
timeout_secs: An integer or a float, the timeout for acquiring the
lock file. 0 indicates non-block.
Returns:
True if the file is locked successfully. False if timeout.
Raises:
OSError: if any file operation fails.
"""
try:
if timeout_secs > 0:
wrapper = utils.TimeoutException(timeout_secs)
wrapper(fcntl.flock)(self._file_desc, fcntl.LOCK_EX)
else:
fcntl.flock(self._file_desc, fcntl.LOCK_EX | fcntl.LOCK_NB)
except errors.FunctionTimeoutError as e:
logger.debug("Cannot lock %s within %s seconds",
self._file_path, timeout_secs)
return False
except (OSError, IOError) as e:
# flock raises IOError in python2; OSError in python3.
if e.errno in (errno.EACCES, errno.EAGAIN):
logger.debug("Cannot lock %s", self._file_path)
return False
raise
return True
def Lock(self, timeout_secs=_DEFAULT_TIMEOUT_SECS):
"""Acquire the lock file.
Args:
timeout_secs: An integer or a float, the timeout for acquiring the
lock file. 0 indicates non-block.
Returns:
True if the file is locked successfully. False if timeout.
Raises:
OSError: if any file operation fails.
"""
if self._file_desc is not None:
raise OSError("%s has been locked." % self._file_path)
parent_dir = os.path.dirname(self._file_path)
if not os.path.exists(parent_dir):
os.makedirs(parent_dir)
successful = False
self._file_desc = os.open(self._file_path, os.O_CREAT | os.O_RDWR,
0o666)
try:
successful = self._Flock(timeout_secs)
finally:
if not successful:
os.close(self._file_desc)
self._file_desc = None
return successful
def _CheckFileDescriptor(self):
"""Raise an error if the file is not opened or locked."""
if self._file_desc is None:
raise RuntimeError("%s has not been locked." % self._file_path)
def SetInUse(self, in_use):
"""Write the instance state to the file.
Args:
in_use: A boolean, whether to set the instance to be in use.
Raises:
OSError: if any file operation fails.
"""
self._CheckFileDescriptor()
os.lseek(self._file_desc, 0, os.SEEK_SET)
state = _IN_USE_STATE if in_use else _NOT_IN_USE_STATE
if os.write(self._file_desc, state) != _LOCK_FILE_SIZE:
raise OSError("Cannot write " + self._file_path)
def Unlock(self):
"""Unlock the file.
Raises:
OSError: if any file operation fails.
"""
self._CheckFileDescriptor()
fcntl.flock(self._file_desc, fcntl.LOCK_UN)
os.close(self._file_desc)
self._file_desc = None
def LockIfNotInUse(self, timeout_secs=_DEFAULT_TIMEOUT_SECS):
"""Lock the file if the instance is not in use.
Returns:
True if the file is locked successfully.
False if timeout or the instance is in use.
Raises:
OSError: if any file operation fails.
"""
if not self.Lock(timeout_secs):
return False
in_use = True
try:
in_use = os.read(self._file_desc, _LOCK_FILE_SIZE) == _IN_USE_STATE
finally:
if in_use:
self.Unlock()
return not in_use