SQLite Access

짜증을 유발하는 SQLite 접근 방식

개요

0.1.0 버전 부터 공유 기능을 추가함으로써 Sub Thread도 DB로부터 공유 데이터를 갖고오기 위해 DB에 접근을 하기 시작합니다.

MySQL, MariaDB 버전은 아무런 문제가 없으나 내부 데이터베이스인 SQLite 버전에서는 문제가 생기기 시작합니다. 다른 쓰레드로 부터 동시 접근 과정에서 SQLite의 데이터는 db파일로 관리되기 때문에 결국 한쪽 쓰레드는 데이터를 받지만, 받는 동안 파일이 잠기게 되 다른 쓰레드가 접근을 못하게 되고 이로 인해 DJango 자체 내에서 Table Lock Error가 발생하게 됩니다. 이걸 조금이나마 해결할 수 있는 솔루션이 필요해 보입니다.

실제 Production에서는 Sub Thread의 DB Access간격이 1일 이상이기 때문에 충돌 가능성은 적지만 TDD를 진행할 때 만료된 데이터를 확인하기 위해 DB Access 간격을 0.5sec로 설정하고 테스트를 합니다. 이러한 짧은 간격으로 인해 DB 접근 관련 에러가 발생하고 있습니다.

InternalDatabaseConcurrencyManager

특징

SQLite 접근 시 순차적으로 접근할 수 있게 Access 조절해주는 클래스 입니다. 주로 Decorator Method로 작동합니다. 하지만 MySQL/MariaDB에서는 해당 기능이 작동되지는 않고 무시됩니다.

    def lock_db_process(self):
        if self.config.rdbms_type == SystemConfig.INTERNAL:
            # SQLite일 경우만 동작한다
            self.process_locker.acquire()

Thread Lock을 이용하여 접근을 중계하기 때문에 반드시 하나의 프로세스에 단 하나의 인스턴스만 생성되어야 합니다. 다라서 singletone이 적용되었습니다. 참고

    def __new__(cls, config: SystemConfig):
        # Singletone 기법으로 작동한다.
        if not hasattr(cls, 'database_concurrency_manager_instance'):
            cls.database_concurrency_manager_instance = \
                super(InternalDatabaseConcurrencyManager, cls).__new__(cls)
        return cls.database_concurrency_manager_instance

Decorator Function

하나의 인스턴스로만 생성 되어야 하지만 여러 클래스에 해당 기능이 작동되게 해야 합니다. 따라서 Decorator Method를 도입하였습니다.

    def manage_internal_transaction(self, func):
        # decoratro function
        lock = self.lock_db_process
        unlock = self.unlock_db_process

        def wrapper(instance: object, *args, **kwargs):
            result = None
            try:
                lock()
                result = func(instance, *args, **kwargs)
            except Exception as e:
                unlock()
                raise e
            else:
                unlock()
                return result

        return wrapper
  • line 3, 4: Thread Lock, Unlock Process 입니다.

  • line 9: 메인 메소드가 실행되기 전에 Lock을 걸어버립니다.

  • line 10: 메인 함수가 실행되고 이에 대한 Output을 리턴해야 하기 때문에 리턴 값을 미리 받습니다.

  • line 12, 15: 함수 실행이 끝났으면 Lock을 해제합니다.

  • line 16: Exception없이 처리되었다면 미리 받아두었던 리턴값을 리턴합니다.

사용 예

주로 Database에 직접 접근하는 1계층(Data/Format Layer)에 적용됩니다. 참고

@staticmethod
@InternalDatabaseConcurrencyManager(SystemConfig()).manage_internal_transaction
def get_shared_file_for_shared_manager_queue(system_root: str, end_date: datetime) -> List:
    r = []
    try:
        for item in model.SharedFile.objects.filter(start_date__lte=end_date):
            r.append(SharedFileData.init_shared_data_from_database(system_root, item))
    except django.db.utils.OperationalError:
        return []
    except Exception as e:
        raise e
    else:
        return r

해당 함수는 공유된 파일들을 DB로부터 불러오는 함수인데, line 6에서 model를 이용하여 직접 DB에 접근하고 있으므로 line2에서 Decorator Method로 사용이 되고 있습니다.

어차피 Singletone으로 구현이 되어 있기 때문에 생성자 함수를 사용해도 새로 생성하는 것이 아닌 이미 만들어진 Instance를 불러오는 것 뿐이므로 기능이 정상작동 됩니다.

하지 여전히 발생되는 문제

확실이 충돌 에러는 줄어들고 있지만 아직 완벽하게 해결되지는 않았습니다. Class 자체의 문제인지, 아직 함수를 추가하지 않은 DB Access Routine이 있는지 아니면, UserManager Legacy Code 때문인지, 계속 연구해 볼 필요가 있습니다.

Last updated