Repository: ihnokim/codepack (language: python)

Issue:
Feature scheduler refactoring



Discussion / hints:

Fix patch:
diff --git a/codepack/base/__init__.py b/codepack/base/__init__.py
new file mode 100644
index 0000000..74f26c9
--- /dev/null
+++ b/codepack/base/__init__.py
@@ -0,0 +1,2 @@
+from codepack.base.code_base import CodeBase
+from codepack.base.codepack_base import CodePackBase
diff --git a/codepack/base/code_base.py b/codepack/base/code_base.py
new file mode 100644
index 0000000..d4decfb
--- /dev/null
+++ b/codepack/base/code_base.py
@@ -0,0 +1,9 @@
+from codepack.storage import Storable
+from codepack.snapshot import Snapshotable
+import abc
+
+
+class CodeBase(Storable, Snapshotable, metaclass=abc.ABCMeta):
+    @abc.abstractmethod
+    def __init__(self, id=None, serial_number=None):
+        Storable.__init__(self, id=id, serial_number=serial_number)
diff --git a/codepack/base/codepack_base.py b/codepack/base/codepack_base.py
new file mode 100644
index 0000000..4228c78
--- /dev/null
+++ b/codepack/base/codepack_base.py
@@ -0,0 +1,9 @@
+from codepack.storage import Storable
+from codepack.snapshot import Snapshotable
+import abc
+
+
+class CodePackBase(Storable, Snapshotable, metaclass=abc.ABCMeta):
+    @abc.abstractmethod
+    def __init__(self, id=None, serial_number=None):
+        Storable.__init__(self, id=id, serial_number=serial_number)
diff --git a/codepack/code.py b/codepack/code.py
index de04fe0..2f6bcf3 100644
--- a/codepack/code.py
+++ b/codepack/code.py
@@ -1,18 +1,21 @@
 import inspect
 from collections.abc import Iterable, Callable
 import dill
-from codepack.service import DefaultServicePack
-from codepack.abc import CodeBase
+from codepack.service import DefaultService
+from codepack.base import CodeBase
 import re
 import ast
 from collections import OrderedDict
 from datetime import datetime
 from codepack.utils.dependency import Dependency
+from codepack.utils.dependency_state import DependencyState
+from codepack.snapshot import CodeSnapshot
 
 
 class Code(CodeBase):
     def __init__(self, function=None, source=None, id=None, serial_number=None, dependency=None,
-                 config_path=None, delivery_service=None, state_manager=None, storage_service=None, state=None):
+                 config_path=None, delivery_service=None, snapshot_service=None, storage_service=None,
+                 state=None, callback=None):
         super().__init__(id=id, serial_number=serial_number)
         self.function = None
         self.source = None
@@ -22,8 +25,9 @@ def __init__(self, function=None, source=None, id=None, serial_number=None, depe
         self.dependency = None
         self.config_path = None
         self.service = None
+        self.callback = None
         self.init_service(delivery_service=delivery_service,
-                          state_manager=state_manager,
+                          snapshot_service=snapshot_service,
                           storage_service=storage_service,
                           config_path=config_path)
         self.set_function(function=function, source=source)
@@ -31,6 +35,7 @@ def __init__(self, function=None, source=None, id=None, serial_number=None, depe
             self.id = self.function.__name__
         self.init_linkage()
         self.init_dependency(dependency=dependency)
+        self.register(callback=callback)
         self.update_state(state)
 
     def init_linkage(self):
@@ -42,15 +47,18 @@ def init_dependency(self, dependency=None):
         if dependency:
             self.add_dependency(dependency=dependency)
 
-    def init_service(self, delivery_service=None, state_manager=None, storage_service=None, config_path=None):
+    def init_service(self, delivery_service=None, snapshot_service=None, storage_service=None, config_path=None):
         self.service = dict()
         self.service['delivery_service'] =\
-            delivery_service if delivery_service else DefaultServicePack.get_default_delivery_service(config_path=config_path)
-        self.service['state_manager'] =\
-            state_manager if state_manager else DefaultServicePack.get_default_state_manager(config_path=config_path)
+            delivery_service if delivery_service else DefaultService.get_default_delivery_service(config_path=config_path)
+        self.service['snapshot_service'] =\
+            snapshot_service if snapshot_service else DefaultService.get_default_code_snapshot_service(config_path=config_path)
         self.service['storage_service'] =\
-            storage_service if storage_service else DefaultServicePack.get_default_code_storage_service(obj=self.__class__,
-                                                                                                        config_path=config_path)
+            storage_service if storage_service else DefaultService.get_default_code_storage_service(obj=self.__class__,
+                                                                                                    config_path=config_path)
+
+    def register(self, callback):
+        self.callback = callback
 
     @staticmethod
     def get_source(function):
@@ -129,6 +137,14 @@ def __floordiv__(self, other):
             raise TypeError(type(other))  # pragma: no cover
         return other
 
+    def update_serial_number(self, serial_number):
+        for c in self.children.values():
+            if self.serial_number in c.dependency:
+                d = c.dependency.pop(self.serial_number)
+                d.serial_number = serial_number
+                c.add_dependency(d)
+        self.serial_number = serial_number
+
     def get_args(self):
         ret = OrderedDict()
         argspec = inspect.getfullargspec(self.function)
@@ -167,27 +183,35 @@ def get_info(self, state=True):
                           self.get_dependent_args())
 
     def __str__(self):
-        return self.get_info(state=True)  # pragma: no cover
+        return self.get_info(state=False)  # pragma: no cover
 
     def __repr__(self):
         return self.__str__()  # pragma: no cover
 
-    def update_state(self, state, update_time=None, args=None, kwargs=None):
+    def update_state(self, state, timestamp=None, args=None, kwargs=None):
         if state:
-            self.service['state_manager'].set(id=self.id, serial_number=self.serial_number, state=state,
-                                              update_time=update_time, args=args, kwargs=kwargs, dependency=self.dependency)
+            snapshot = self.to_snapshot(args=args, kwargs=kwargs, timestamp=timestamp)
+            snapshot['state'] = state
+            self.service['snapshot_service'].save(snapshot)
+            if self.callback:
+                self.callback({'serial_number': self.serial_number, 'state': state})
 
     def get_state(self):
-        return self.service['state_manager'].get(serial_number=self.serial_number)
+        ret = self.service['snapshot_service'].load(serial_number=self.serial_number, projection={'state'})
+        if ret:
+            return ret['state']
+        else:
+            return 'UNKNOWN'
 
-    def send_result(self, item, send_time=None):
-        self.service['delivery_service'].send(sender=self.id, invoice_number=self.serial_number, item=item, send_time=send_time)
+    def send_result(self, item, timestamp=None):
+        self.service['delivery_service'].send(id=self.id, serial_number=self.serial_number, item=item, timestamp=timestamp)
 
-    def get_result(self, serial_number):
-        return self.service['delivery_service'].receive(invoice_number=serial_number)
+    def get_result(self, serial_number=None):
+        serial_number = serial_number if serial_number else self.serial_number
+        return self.service['delivery_service'].receive(serial_number=serial_number)
 
-    def save(self, *args, **kwargs):
-        self.service['storage_service'].save(self, *args, **kwargs)
+    def save(self):
+        self.service['storage_service'].save(item=self)
 
     def receive(self, arg):
         self.assert_arg(arg)
@@ -221,70 +245,76 @@ def add_dependency(self, dependency):
     def remove_dependency(self, serial_number):
         self.dependency.pop(serial_number, None)
 
-    def get_dependency_state_info(self):
+    def get_dependent_snapshots(self):
         ret = dict()
-        states = self.service['state_manager'].check(serial_number=list(self.dependency.keys()))
-        for state in states:
-            ret[state['_id']] = state
+        snapshots = self.service['snapshot_service'].load(serial_number=list(self.dependency.keys()))
+        for snapshot in snapshots:
+            ret[snapshot['_id']] = snapshot
         return ret
 
     def get_dependency_cache_info(self):
         ret = dict()
-        caches = self.service['delivery_service'].check(invoice_number=[k for k, v in self.dependency.items() if v.arg])
+        caches = self.service['delivery_service'].check(serial_number=[k for k, v in self.dependency.items() if v.arg])
         for cache in caches:
             ret[cache['_id']] = cache
         return ret
 
-    def check_dependency_termination(self, states):
-        if len(self.dependency) != len(states):
-            return False
-        for state in states.values():
-            if state['state'] != 'TERMINATED':
-                return False
-        return True
-
-    def validate_dependency_result(self, states, caches):
+    def check_dependency_state(self, snapshots):
+        for snapshot in snapshots.values():
+            if snapshot['state'] == 'ERROR':
+                return DependencyState.ERROR
+            elif snapshot['state'] != 'TERMINATED':
+                return DependencyState.NOT_READY
+        if len(self.dependency) != len(snapshots):
+            return DependencyState.NOT_READY
+        return DependencyState.READY
+
+    def validate_dependency_result(self, snapshots, caches):
         if len([k for k, v in self.dependency.items() if v.arg]) != len(caches):
-            return False
+            return DependencyState.NOT_READY
         for cache in caches.values():
-            if cache['_id'] not in states:
-                return False
-            elif 'send_time' not in cache or 'update_time' not in states[cache['_id']]:
-                return False
-            elif cache['send_time'] != states[cache['_id']]['update_time']:
-                return False
-        return True
+            if cache['_id'] not in snapshots:
+                return DependencyState.NOT_READY
+            elif 'timestamp' not in cache or 'timestamp' not in snapshots[cache['_id']]:
+                return DependencyState.NOT_READY
+            elif cache['timestamp'] != snapshots[cache['_id']]['timestamp']:
+                return DependencyState.NOT_READY
+        return DependencyState.READY
 
     def check_dependency(self):
-        states = self.get_dependency_state_info()
-        if not self.check_dependency_termination(states=states):
-            return False
+        snapshots = self.get_dependent_snapshots()
+        dependency_state = self.check_dependency_state(snapshots=snapshots)
+        if dependency_state != 'READY':
+            return dependency_state
         caches = self.get_dependency_cache_info()
-        if not self.validate_dependency_result(states=states, caches=caches):
-            return False
-        return True
+        return self.validate_dependency_result(snapshots=snapshots, caches=caches)
 
     def __call__(self, *args, **kwargs):
         try:
-            self.update_state('READY')
-            if self.check_dependency():
+            dependency_state = self.check_dependency()
+            if dependency_state == 'READY':
                 for dependency in self.dependency.values():
                     if dependency.arg and dependency.arg not in kwargs:
                         kwargs[dependency.arg] = self.get_result(serial_number=dependency.serial_number)
-                self.update_state('RUNNING')
+                self.update_state('RUNNING', args=args, kwargs=kwargs)
                 ret = self.function(*args, **kwargs)
                 now = datetime.now().timestamp()
-                self.send_result(item=ret, send_time=now)
-                self.update_state('TERMINATED', update_time=now)
+                self.send_result(item=ret, timestamp=now)
+                self.update_state('TERMINATED', args=args, kwargs=kwargs, timestamp=now)
                 return ret
-            else:
+            elif dependency_state == 'NOT_READY':
                 self.update_state('WAITING', args=args, kwargs=kwargs)
                 return None
+            elif dependency_state == 'ERROR':
+                self.update_state('ERROR', args=args, kwargs=kwargs)
+                return None
+            else:
+                raise NotImplementedError(dependency_state)  # pragma: no cover
         except Exception as e:
-            self.update_state('ERROR')
+            self.update_state('ERROR', args=args, kwargs=kwargs)
             raise e
 
-    def to_dict(self, *args, **kwargs):
+    def to_dict(self):
         d = dict()
         d['_id'] = self.id
         d['source'] = self.source
@@ -292,5 +322,13 @@ def to_dict(self, *args, **kwargs):
         return d
 
     @classmethod
-    def from_dict(cls, d, *args, **kwargs):
-        return cls(id=d['_id'], source=d['source'], *args, **kwargs)
+    def from_dict(cls, d):
+        return cls(id=d['_id'], source=d['source'])
+
+    def to_snapshot(self, *args, **kwargs):
+        return CodeSnapshot(self, *args, **kwargs)
+
+    @classmethod
+    def from_snapshot(cls, snapshot):
+        return cls(id=snapshot['id'], serial_number=snapshot['serial_number'],
+                   state=snapshot['state'], dependency=snapshot['dependency'], source=snapshot['source'])
diff --git a/codepack/codepack.py b/codepack/codepack.py
index ed894d8..6e48b81 100644
--- a/codepack/codepack.py
+++ b/codepack/codepack.py
@@ -1,17 +1,19 @@
-from codepack.abc import CodeBase, CodePackBase
+from codepack.base import CodeBase, CodePackBase
 from codepack import Code
 from queue import Queue
-from copy import deepcopy
+from codepack.service import DefaultService
+from codepack.utils.state import State
 from parse import compile as parser
 from ast import literal_eval
+from codepack.snapshot import CodePackSnapshot
 
 
 class CodePack(CodePackBase):
-    def __init__(self, id, code, subscribe=None):
-        super().__init__(id=id)
+    def __init__(self, id, code, subscribe=None, serial_number=None, config_path=None, snapshot_service=None, storage_service=None):
+        super().__init__(id=id, serial_number=serial_number)
         self.root = None
         self.roots = None
-        self.arg_cache = None
+        self.service = None
         self.set_root(code)
         if isinstance(subscribe, CodeBase):
             self.subscribe = subscribe.id
@@ -20,28 +22,23 @@ def __init__(self, id, code, subscribe=None):
         else:
             self.subscribe = None
         self.codes = dict()
-        self.init(arg_dict=None, lazy=False)
-
-    def init(self, arg_dict=None, lazy=False):
-        self.init_arg_cache(arg_dict, lazy=lazy)
-        if not lazy:
-            self.roots = self.get_roots(init=True)
-
-    def init_arg_cache(self, arg_dict, lazy=False):
-        if arg_dict and lazy:
-            q = Queue()
-            for id in self.arg_cache:
-                if self.arg_cache[id] != arg_dict[id]:
-                    q.put(id)
-            while not q.empty():
-                id = q.get()
-                self.arg_cache.pop(id, None)
-                for c in self.codes[id].children.values():
-                    q.put(c.id)
-        else:
-            self.arg_cache = dict()
-
-    def make_arg_dict(self):
+        self.init()
+        self.init_service(snapshot_service=snapshot_service,
+                          storage_service=storage_service,
+                          config_path=config_path)
+
+    def init(self):
+        self.roots = self.get_roots()
+
+    def init_service(self, snapshot_service=None, storage_service=None, config_path=None):
+        self.service = dict()
+        self.service['snapshot_service'] =\
+            snapshot_service if snapshot_service else DefaultService.get_default_codepack_snapshot_service(config_path=config_path)
+        self.service['storage_service'] =\
+            storage_service if storage_service else DefaultService.get_default_codepack_storage_service(obj=self.__class__,
+                                                                                                        config_path=config_path)
+
+    def make_argpack(self):
         ret = dict()
         stack = list()
         for root in self.roots:
@@ -83,6 +80,33 @@ def __str__(self):
     def __repr__(self):
         return self.__str__()
 
+    def update_state(self, state, timestamp=None, argpack=None):
+        if state:
+            snapshot = self.to_snapshot(argpack=argpack, timestamp=timestamp)
+            snapshot['state'] = state
+            self.service['snapshot_service'].save(snapshot)
+
+    def get_state(self):
+        states = self.root.service['snapshot_service']\
+            .load(serial_number=[code.serial_number for code in self.codes.values()], projection={'state'})
+        if states:
+            max_state = 0
+            terminated = True
+            for state in states:
+                if state['state'] == 'ERROR':
+                    return 'ERROR'
+                elif state['state'] != 'TERMINATED':
+                    terminated = False
+                    max_state = max(State[state['state']].value, max_state)
+            if terminated:
+                return 'TERMINATED'
+            else:
+                return State(max_state).name
+        return 'UNKNOWN'
+
+    def save(self):
+        self.service['storage_service'].save(item=self)
+
     def get_leaves(self):
         leaves = set()
         q = Queue()
@@ -95,7 +119,7 @@ def get_leaves(self):
                 leaves.add(n)
         return leaves
 
-    def get_roots(self, init=False):
+    def get_roots(self):
         roots = set()
         touched = set()
         q = Queue()
@@ -104,11 +128,8 @@ def get_roots(self, init=False):
             touched.add(leave.id)
         while not q.empty():
             n = q.get()
-            if init:
-                if n.get_state() != 'NEW':
-                    n.update_state('NEW')
-                if n.id not in self.codes:
-                    self.codes[n.id] = n
+            if n.id not in self.codes:
+                self.codes[n.id] = n
             for p in n.parents.values():
                 if p.id not in touched:
                     q.put(p)
@@ -117,78 +138,88 @@ def get_roots(self, init=False):
                 roots.add(n)
         return roots
 
-    def recursive_run(self, code, arg_dict):
-        state = code.get_state()
-        senders = code.get_dependent_args().values()
-        redo = True if state != 'TERMINATED' else False
+    def recursive_run(self, code, argpack):
         for p in code.parents.values():
-            if p.get_state() != 'TERMINATED' or \
-                    p.id not in self.arg_cache or \
-                    arg_dict[p.id] != self.arg_cache[p.id]:
-                redo = True if p.id in senders else False
+            if p.get_state() != 'TERMINATED':
                 code.update_state('WAITING')
-                self.recursive_run(p, arg_dict)
-        if redo or \
-                code.id not in self.arg_cache or \
-                arg_dict[code.id] != self.arg_cache[code.id]:
-            self.arg_cache[code.id] = deepcopy(arg_dict[code.id])
-            code.update_state('READY')
-            code(**arg_dict[code.id])
-
-    def __call__(self, arg_dict=None, lazy=False):
-        ret = None
-        if not arg_dict:
-            arg_dict = self.make_arg_dict()
-        self.init(arg_dict=arg_dict if lazy else None, lazy=lazy)
+                self.recursive_run(p, argpack)
+        code.update_state('READY')
+        code(**argpack[code.id])
+
+    def sync_run(self, argpack):
         for leave in self.get_leaves():
-            self.recursive_run(leave, arg_dict)
-        if self.subscribe:
-            c = self.codes[self.subscribe]
-            ret = c.get_result(serial_number=c.serial_number)
-        return ret
+            self.recursive_run(leave, argpack)
+
+    def async_run(self, argpack=None):
+        for id, code in self.codes.items():
+            code(**argpack[id])
+
+    def __call__(self, argpack=None, sync=True):
+        self.init_code_state(state='READY', argpack=argpack)
+        self.update_state('READY', argpack=argpack)
+        if not argpack:
+            argpack = self.make_argpack()
+        if sync:
+            self.sync_run(argpack=argpack)
+            self.update_state('TERMINATED', argpack=argpack)
+        else:
+            self.async_run(argpack=argpack)
+            self.update_state(self.get_state(), argpack=argpack)
+        return self.get_result()
+
+    def init_code_state(self, state, argpack=None):
+        for id, code in self.codes.items():
+            if argpack and id in argpack:
+                code.update_state(state, kwargs=argpack[id])
+
+    def get_result(self):
+        if self.subscribe and self.codes[self.subscribe].get_state() == 'TERMINATED':
+            return self.codes[self.subscribe].get_result()
+        else:
+            return None
 
-    def to_dict(self, *args, **kwargs):
+    def to_dict(self):
         d = dict()
         d['_id'] = self.id
         d['subscribe'] = self.subscribe
         d['structure'] = self.get_structure()
-        d['source'] = {id: code.source for id, code in self.codes.items()}
+        d['source'] = self.get_source()
         return d
 
     @classmethod
-    def from_dict(cls, d, *args, **kwargs):
+    def from_dict(cls, d):
         p = parser('Code(id: {id}, function: {function}, args: {args}, receive: {receive})')
         root = None
         stack = list()
         codes = dict()
-
-        for i, line in enumerate(d['structure'].split('\n')): # os.linesep
+        receive = dict()
+        for i, line in enumerate(d['structure'].split('\n')):  # os.linesep
             split_idx = line.index('Code')
             hierarchy = len(line[1: split_idx-1])
             attr = p.parse(line[split_idx:])
-
             if attr['id'] not in codes:
                 codes[attr['id']] = Code(id=attr['id'], source=d['source'][attr['id']])
-
             code = codes[attr['id']]
-            receive = literal_eval(attr['receive'])
-            for arg, sender in receive.items():
-                code.receive(arg) << sender
+            receive[code.id] = literal_eval(attr['receive'])
             if i == 0:
                 root = code
-
             while len(stack) and stack[-1][1] >= hierarchy:
                 n, h = stack.pop(-1)
                 if len(stack) > 0:
                     stack[-1][0] >> n
             stack.append((code, hierarchy))
-
         while len(stack):
             n, h = stack.pop(-1)
             if len(stack) > 0:
                 stack[-1][0] >> n
+        for id, code in codes.items():
+            for arg, sender in receive[id].items():
+                code.receive(arg) << codes[sender]
         return cls(d['_id'], code=root, subscribe=d['subscribe'])
 
+    def get_source(self):
+        return {id: code.source for id, code in self.codes.items()}
+
     def get_structure(self):
         ret = str()
         stack = list()
@@ -206,3 +237,16 @@ def get_structure(self):
                 for c in n.children.values():
                     stack.append((c, h + 1))
         return ret
+
+    def to_snapshot(self, *args, **kwargs):
+        return CodePackSnapshot(self, *args, **kwargs)
+
+    @classmethod
+    def from_snapshot(cls, snapshot):
+        d = snapshot.to_dict()
+        d['_id'] = d['id']
+        ret = cls.from_dict(d)
+        ret.serial_number = d['serial_number']
+        for id, code in ret.codes.items():
+            code.update_serial_number(d['codes'][id])
+        return ret
diff --git a/codepack/engine/__init__.py b/codepack/engine/__init__.py
new file mode 100644
index 0000000..d60e52a
--- /dev/null
+++ b/codepack/engine/__init__.py
@@ -0,0 +1,3 @@
+from codepack.engine.engine import Engine
+from codepack.engine.blocking_engine import BlockingEngine
+from codepack.engine.non_blocking_engine import NonBlockingEngine
diff --git a/codepack/engine/blocking_engine.py b/codepack/engine/blocking_engine.py
new file mode 100644
index 0000000..6e7deb0
--- /dev/null
+++ b/codepack/engine/blocking_engine.py
@@ -0,0 +1,20 @@
+from codepack.engine import Engine
+import time
+
+
+class BlockingEngine(Engine):
+    def __init__(self, callback, interval=1, config_path=None, snapshot_service=None):
+        super().__init__(callback=callback, interval=interval, config_path=config_path, snapshot_service=snapshot_service)
+
+    def start(self):
+        while True:
+            try:
+                time.sleep(self.interval)
+                self.work()
+            except KeyboardInterrupt:
+                break
+            finally:
+                self.stop()
+
+    def stop(self):
+        pass
diff --git a/codepack/engine/engine.py b/codepack/engine/engine.py
new file mode 100644
index 0000000..a99fb8f
--- /dev/null
+++ b/codepack/engine/engine.py
@@ -0,0 +1,28 @@
+import abc
+from codepack.service import DefaultService
+
+
+class Engine(metaclass=abc.ABCMeta):
+    def __init__(self, callback, interval=1, config_path=None, snapshot_service=None):
+        """initialize instance"""
+        self.callback = callback
+        self.interval = interval
+        self.snapshot_service = snapshot_service if snapshot_service else DefaultService.get_default_code_snapshot_service(config_path=config_path)
+
+    @abc.abstractmethod
+    def start(self):
+        """Start loop"""
+
+    @abc.abstractmethod
+    def stop(self):
+        """Stop loop"""
+
+    def work(self):
+        for x in self.snapshot_service.search(key='state', value='WAITING'):
+            resolved = True
+            for s in self.snapshot_service.load([x['serial_number'] for x in x['dependency']], projection={'state'}):
+                if s['state'] != 'TERMINATED':
+                    resolved = False
+                    break
+            if resolved:
+                self.callback(x)
diff --git a/codepack/engine/non_blocking_engine.py b/codepack/engine/non_blocking_engine.py
new file mode 100644
index 0000000..77bc7cc
--- /dev/null
+++ b/codepack/engine/non_blocking_engine.py
@@ -0,0 +1,14 @@
+from codepack.engine import Engine
+from codepack.utils import Looper
+
+
+class NonBlockingEngine(Engine):
+    def __init__(self, callback, interval=1, daemon=True, config_path=None, snapshot_service=None):
+        super().__init__(callback=callback, interval=interval, config_path=config_path, snapshot_service=snapshot_service)
+        self.worker = Looper(self.work, interval=interval, daemon=daemon)
+
+    def start(self):
+        self.worker.start()
+
+    def stop(self):
+        self.worker.stop()
diff --git a/codepack/interface/__init__.py b/codepack/interface/__init__.py
index c18e55a..f5a9c54 100644
--- a/codepack/interface/__init__.py
+++ b/codepack/interface/__init__.py
@@ -1,3 +1,5 @@
+from codepack.interface.interface import Interface
+from codepack.interface.sql_interface import SQLInterface
 from codepack.interface.mongodb import MongoDB
 from codepack.interface.mysql import MySQL
 from codepack.interface.mssql import MSSQL
diff --git a/codepack/interface/dynamodb.py b/codepack/interface/dynamodb.py
index aa82848..a878548 100644
--- a/codepack/interface/dynamodb.py
+++ b/codepack/interface/dynamodb.py
@@ -1,29 +1,24 @@
 import boto3
 from botocore.config import Config
-from codepack.interface.abc import SQLInterface
+from codepack.interface import SQLInterface
 from boto3.dynamodb.types import TypeDeserializer
 
 
 class DynamoDB(SQLInterface):
-    def __init__(self, config, **kwargs):
-        super().__init__()
-        self.client = None
+    def __init__(self, config, *args, **kwargs):
+        super().__init__(config)
         self.td = TypeDeserializer()
-        try:
-            self.client = self.connect(config=config, **kwargs)
-        except Exception as e:
-            print(e)
-            self.client = None
+        self.connect(*args, **kwargs)
 
-    def connect(self, config, ssh_config=None, **kwargs):
-        self.config = config
-        return boto3.client(config=Config(retries=dict(max_attempts=3)), **self.config, **kwargs)
+    def connect(self, *args, **kwargs):
+        self.session = boto3.client(config=Config(retries=dict(max_attempts=3)), *args, **self.config, **kwargs)
+        return self.session
 
     def list_tables(self, name):
         ret = list()
-        if self.client is None:
+        if self.session is None:
             return ret
-        return self.client.list_tables(ExclusiveStartTableName=name)['TableNames']
+        return self.session.list_tables(ExclusiveStartTableName=name)['TableNames']
 
     def query(self, table, q, columns=None, preprocess=None, preprocess_args=None, preprocess_kwargs=None, dummy_column='dummy'):
         if preprocess is None:
@@ -41,7 +36,7 @@ def query(self, table, q, columns=None, preprocess=None, preprocess_args=None, p
         while not done:
             if start_key:
                 params['ExclusiveStartKey'] = start_key
-            response = self.client.query(**params)
+            response = self.session.query(**params)
             items = [{k: preprocess(self.td.deserialize(v), *preprocess_args, **preprocess_kwargs) for k, v in item.items()}
                      for item in response.get('Items', list()) if dummy_column not in item]
             start_key = response.get('LastEvaluatedKey', None)
@@ -57,7 +52,7 @@ def select(self, table, columns=None, preprocess=None, preprocess_args=None, pre
                           dummy_column=dummy_column)
 
     def describe_table(self, table):
-        return self.client.describe_table(TableName=table)['Table']
+        return self.session.describe_table(TableName=table)['Table']
 
     @staticmethod
     def array_parser(s, sep='\x7f', dtype=str):
diff --git a/codepack/interface/interface.py b/codepack/interface/interface.py
new file mode 100644
index 0000000..163618f
--- /dev/null
+++ b/codepack/interface/interface.py
@@ -0,0 +1,61 @@
+import abc
+from sshtunnel import SSHTunnelForwarder
+from codepack.utils.config import get_config
+from copy import deepcopy
+
+
+class Interface(metaclass=abc.ABCMeta):
+    def __init__(self, config):
+        self.config = None
+        self.ssh_config = None
+        self.ssh = None
+        self.session = None
+        self.closed = True
+        self.init_config(config)
+
+    def init_config(self, config):
+        _config = deepcopy(config)
+        if 'sshtunnel' in _config:
+            _ssh_config = _config.pop('sshtunnel')
+            if isinstance(_ssh_config, str):
+                config_path, section = _ssh_config.split(':')
+                self.ssh_config = get_config(filename=config_path, section=section)
+            elif isinstance(_ssh_config, dict):
+                self.ssh_config = _ssh_config
+            else:
+                raise TypeError(type(_ssh_config))
+        self.config = _config
+
+    @abc.abstractmethod
+    def connect(self, *args, **kwargs):
+        """connect to the server"""
+
+    @abc.abstractmethod
+    def close(self):
+        """close the connection to the server"""
+
+    @staticmethod
+    def exclude_keys(d, keys):
+        return {k: v for k, v in d.items() if k not in keys}
+
+    def bind(self, host, port):
+        if self.ssh_config:
+            _ssh_config = self.exclude_keys(self.ssh_config, keys=['ssh_host', 'ssh_port'])
+            self.ssh = SSHTunnelForwarder((self.ssh_config['ssh_host'], int(self.ssh_config['ssh_port'])),
+                                          remote_bind_address=(host, int(port)),
+                                          **_ssh_config)
+            self.ssh.start()
+            _host = '127.0.0.1'
+            _port = self.ssh.local_bind_port
+        else:
+            _host = host
+            _port = port
+        return _host, int(_port)
+
+    @staticmethod
+    def eval_bool(source):
+        assert source in ['True', 'False', True, False], "'source' should be either 'True' or 'False'"
+        if type(source) == str:
+            return eval(source)
+        else:
+            return source
diff --git a/codepack/interface/mongodb.py b/codepack/interface/mongodb.py
index 7810c63..ec79e9e 100644
--- a/codepack/interface/mongodb.py
+++ b/codepack/interface/mongodb.py
@@ -1,33 +1,31 @@
 from pymongo import MongoClient
-from codepack.interface.abc import Interface
+from codepack.interface import Interface
 
 
 class MongoDB(Interface):
-    def __init__(self, config, ssh_config=None, **kwargs):
-        super().__init__()
-        self.client = None
-        self.connect(config=config, ssh_config=ssh_config, **kwargs)
+    def __init__(self, config, *args, **kwargs):
+        super().__init__(config)
+        self.connect(*args, **kwargs)
 
-    def connect(self, config, ssh_config=None, **kwargs):
-        self.config = config
-        host, port = self.set_sshtunnel(host=self.config['host'], port=self.config['port'], ssh_config=ssh_config)
+    def connect(self, *args, **kwargs):
+        host, port = self.bind(host=self.config['host'], port=self.config['port'])
         _config = self.exclude_keys(self.config, keys=['host', 'port'])
         if 'connect' in _config:
             _config['connect'] = self.eval_bool(_config['connect'])
-        self.client = MongoClient(host=host, port=port, **_config, **kwargs)
+        self.session = MongoClient(host=host, port=port, *args, **_config, **kwargs)
         self.closed = False
-        return self.client
+        return self.session
 
     def __getitem__(self, item):
         assert not self.closed, "connection is closed"
-        return self.client[item]
+        return self.session[item]
 
     def __getattr__(self, item):
         assert not self.closed, "connection is closed"
         return self.__getitem__(item)
 
     def close(self):
-        self.client.close()
+        self.session.close()
         if not self.closed:
             if self.ssh_config and self.ssh is not None:
                 self.ssh.stop()
diff --git a/codepack/interface/mssql.py b/codepack/interface/mssql.py
index ccb1501..0608c55 100644
--- a/codepack/interface/mssql.py
+++ b/codepack/interface/mssql.py
@@ -1,17 +1,15 @@
 import pymssql
-from codepack.interface.abc import SQLInterface
+from codepack.interface import SQLInterface
 
 
 class MSSQL(SQLInterface):
-    def __init__(self, config, ssh_config=None, **kwargs):
-        super().__init__()
-        self.conn = None
+    def __init__(self, config, *args, **kwargs):
+        super().__init__(config)
         self.as_dict = None
-        self.connect(config=config, ssh_config=ssh_config, **kwargs)
+        self.connect(*args, **kwargs)
 
-    def connect(self, config, ssh_config=None, **kwargs):
-        self.config = config
-        host, port = self.set_sshtunnel(host=self.config['host'], port=self.config['port'], ssh_config=ssh_config)
+    def connect(self, *args, **kwargs):
+        host, port = self.bind(host=self.config['host'], port=self.config['port'])
         _config = self.exclude_keys(self.config, keys=['host', 'port'])
         self.as_dict = False
         if 'as_dict' in _config:
@@ -19,16 +17,16 @@ def connect(self, config, ssh_config=None, **kwargs):
         if 'as_dict' in kwargs:
             self.as_dict = self.eval_bool(kwargs['as_dict'])
         _config['as_dict'] = self.as_dict
-        self.conn = pymssql.connect(host=host, port=port, **_config, **kwargs)
+        self.session = pymssql.connect(host=host, port=port, *args, **_config, **kwargs)
         self.closed = False
-        return self.conn
+        return self.session
 
     def query(self, q, commit=False):
         assert not self.closed, "connection is closed"
         columns = None
         rows = None
         try:
-            cursor = self.conn.cursor()
+            cursor = self.session.cursor()
             if type(q) == str:
                 cursor.execute(q)
             elif type(q) == list:
@@ -40,9 +38,9 @@ def query(self, q, commit=False):
                 rows = cursor.fetchall()
             cursor.close()
             if commit:
-                self.conn.commit()
+                self.session.commit()
         except Exception as e:
-            self.conn.rollback()
+            self.session.rollback()
             raise e
         if self.as_dict:
             return rows
@@ -123,7 +121,7 @@ def delete(self, db, table, commit=False, **kwargs):
 
     def close(self):
         if not self.closed:
-            self.conn.close()
+            self.session.close()
             if self.ssh_config and self.ssh is not None:
                 self.ssh.stop()
                 self.ssh = None
diff --git a/codepack/interface/mysql.py b/codepack/interface/mysql.py
index 994ae96..6123823 100644
--- a/codepack/interface/mysql.py
+++ b/codepack/interface/mysql.py
@@ -1,23 +1,21 @@
 import pymysql
-from codepack.interface.abc import SQLInterface
+from codepack.interface import SQLInterface
 import re
 
 
 class MySQL(SQLInterface):
-    def __init__(self, config, ssh_config=None, **kwargs):
-        super().__init__()
-        self.conn = None
-        self.connect(config=config, ssh_config=ssh_config, **kwargs)
-
-    def connect(self, config, ssh_config=None, **kwargs):
-        self.config = config
-        host, port = self.set_sshtunnel(host=self.config['host'], port=self.config['port'], ssh_config=ssh_config)
+    def __init__(self, config, *args, **kwargs):
+        super().__init__(config)
+        self.connect(*args, **kwargs)
+
+    def connect(self, *args, **kwargs):
+        host, port = self.bind(host=self.config['host'], port=self.config['port'])
         _config = self.exclude_keys(self.config, keys=['host', 'port'])
         if 'cursorclass' in _config:
             _config['cursorclass'] = self.eval_cursor_object(_config['cursorclass'])
-        self.conn = pymysql.connect(host=host, port=port, **_config, **kwargs)
+        self.session = pymysql.connect(host=host, port=port, *args, **_config, **kwargs)
         self.closed = False
-        return self.conn
+        return self.session
 
     @staticmethod
     def eval_cursor_object(cursorclass):
@@ -29,17 +27,17 @@ def eval_cursor_object(cursorclass):
             return cursorclass
 
     def commit(self):
-        self.conn.commit()
+        self.session.commit()
 
     def rollback(self):
-        self.conn.rollback()
+        self.session.rollback()
 
     def query(self, q, commit=False):
         assert not self.closed, "connection is closed"
         columns = None
         rows = None
         try:
-            cursor = self.conn.cursor()
+            cursor = self.session.cursor()
             cursorclass = cursor.__class__
             if type(q) == str:
                 cursor.execute(q)
@@ -51,9 +49,9 @@ def query(self, q, commit=False):
             rows = cursor.fetchall()
             cursor.close()
             if commit:
-                self.conn.commit()
+                self.session.commit()
         except Exception as e:
-            self.conn.rollback()
+            self.session.rollback()
             raise e
         if cursorclass in [pymysql.cursors.DictCursor, pymysql.cursors.SSDictCursor]:
             return rows
@@ -136,7 +134,7 @@ def make_call_query(db, procedure, **kwargs):
 
     def close(self):
         if not self.closed:
-            self.conn.close()
+            self.session.close()
             if self.ssh_config and self.ssh is not None:
                 self.ssh.stop()
                 self.ssh = None
diff --git a/codepack/interface/oracledb.py b/codepack/interface/oracledb.py
index 12e2e0e..b7fae50 100644
--- a/codepack/interface/oracledb.py
+++ b/codepack/interface/oracledb.py
@@ -1,5 +1,5 @@
 import cx_Oracle
-from codepack.interface.abc import SQLInterface
+from codepack.interface import SQLInterface
 from functools import partial
 
 
@@ -10,15 +10,13 @@ def make_named_row(names, *args):
 
 
 class OracleDB(SQLInterface):
-    def __init__(self, config, ssh_config=None, **kwargs):
-        super().__init__()
-        self.conn = None
+    def __init__(self, config, *args, **kwargs):
+        super().__init__(config)
         self.as_dict = None
-        self.connect(config=config, ssh_config=ssh_config, **kwargs)
+        self.connect(*args, **kwargs)
 
-    def connect(self, config, ssh_config=None, **kwargs):
-        self.config = config
-        host, port = self.set_sshtunnel(host=self.config['host'], port=self.config['port'], ssh_config=ssh_config)
+    def connect(self, *args, **kwargs):
+        host, port = self.bind(host=self.config['host'], port=self.config['port'])
         exclude_keys = ['host', 'port']
         if 'service_name' in self.config:
             exclude_keys += ['service_name']
@@ -33,16 +31,16 @@ def connect(self, config, ssh_config=None, **kwargs):
             self.as_dict = self.eval_bool(kwargs['as_dict'])
             kwargs = self.exclude_keys(kwargs, keys=['as_dict'])
         _config = self.exclude_keys(self.config, keys=exclude_keys)
-        self.conn = cx_Oracle.connect(dsn=dsn, **_config, **kwargs)
+        self.session = cx_Oracle.connect(dsn=dsn, *args, **_config, **kwargs)
         self.closed = False
-        return self.conn
+        return self.session
 
     def query(self, q, commit=False):
         assert not self.closed, "connection is closed"
         columns = None
         rows = None
         try:
-            cursor = self.conn.cursor()
+            cursor = self.session.cursor()
             if type(q) == str:
                 cursor.execute(q)
             elif type(q) == list:
@@ -55,9 +53,9 @@ def query(self, q, commit=False):
             rows = cursor.fetchall()
             cursor.close()
             if commit:
-                self.conn.commit()
+                self.session.commit()
         except Exception as e:
-            self.conn.rollback()
+            self.session.rollback()
             raise e
         if self.as_dict:
             return rows
@@ -69,7 +67,7 @@ def query(self, q, commit=False):
 
     def close(self):
         if not self.closed:
-            self.conn.close()
+            self.session.close()
             if self.ssh_config and self.ssh is not None:
                 self.ssh.stop()
                 self.ssh = None
diff --git a/codepack/interface/abc.py b/codepack/interface/sql_interface.py
similarity index 55%
rename from codepack/interface/abc.py
rename to codepack/interface/sql_interface.py
index cb87364..f1edb98 100644
--- a/codepack/interface/abc.py
+++ b/codepack/interface/sql_interface.py
@@ -1,49 +1,6 @@
 import abc
 import numpy as np
-from sshtunnel import SSHTunnelForwarder
-
-
-class Interface(metaclass=abc.ABCMeta):
-    def __init__(self):
-        self.config = None
-        self.ssh_config = None
-        self.ssh = None
-        self.closed = True
-
-    @abc.abstractmethod
-    def connect(self, config, ssh_config=None, **kwargs):
-        """connect to the server"""
-
-    @abc.abstractmethod
-    def close(self):
-        """close the connection to the server"""
-
-    @staticmethod
-    def exclude_keys(d, keys):
-        return {k: v for k, v in d.items() if k not in keys}
-
-    def set_sshtunnel(self, host, port, ssh_config):
-        self.ssh_config = ssh_config
-        if self.ssh_config:
-            _ssh_config = self.exclude_keys(ssh_config, keys=['ssh_host', 'ssh_port'])
-            self.ssh = SSHTunnelForwarder((self.ssh_config['ssh_host'], int(self.ssh_config['ssh_port'])),
-                                          remote_bind_address=(host, int(port)),
-                                          **_ssh_config)
-            self.ssh.start()
-            _host = '127.0.0.1'
-            _port = self.ssh.local_bind_port
-        else:
-            _host = host
-            _port = port
-        return _host, int(_port)
-
-    @staticmethod
-    def eval_bool(source):
-        assert source in ['True', 'False', True, False], "'source' should be either 'True' or 'False'"
-        if type(source) == str:
-            return eval(source)
-        else:
-            return source
+from codepack.interface import Interface
 
 
 class SQLInterface(Interface, metaclass=abc.ABCMeta):
diff --git a/codepack/scheduler/__init__.py b/codepack/scheduler/__init__.py
index 6c75521..50639b4 100644
--- a/codepack/scheduler/__init__.py
+++ b/codepack/scheduler/__init__.py
@@ -1,1 +1,2 @@
+from codepack.scheduler.scheduler import Scheduler
 from codepack.scheduler.mongo_scheduler import MongoScheduler
diff --git a/codepack/scheduler/jobstore/__init__.py b/codepack/scheduler/jobstore/__init__.py
new file mode 100644
index 0000000..3b72567
--- /dev/null
+++ b/codepack/scheduler/jobstore/__init__.py
@@ -0,0 +1,1 @@
+from codepack.scheduler.jobstore.mongo_jobstore import MongoJobStore
diff --git a/codepack/scheduler/jobstores/mongodb.py b/codepack/scheduler/jobstore/mongo_jobstore.py
similarity index 72%
rename from codepack/scheduler/jobstores/mongodb.py
rename to codepack/scheduler/jobstore/mongo_jobstore.py
index e8299ca..b389e85 100644
--- a/codepack/scheduler/jobstores/mongodb.py
+++ b/codepack/scheduler/jobstore/mongo_jobstore.py
@@ -1,15 +1,14 @@
 from apscheduler.jobstores.mongodb import *
-from apscheduler.jobstores.mongodb import MongoDBJobStore as MongoDBJobStoreBase
-from datetime import datetime
+from datetime import datetime, timezone
 
 
-class MongoDBJobStore(MongoDBJobStoreBase):
-    def __init__(self, database='apscheduler',
-                 collection='jobs',
+class MongoJobStore(MongoDBJobStore):
+    def __init__(self, db,
+                 collection,
                  client=None,
                  pickle_protocol=pickle.HIGHEST_PROTOCOL,
                  **connect_args):
-        super().__init__(database=database,
+        super().__init__(database=db,
                          collection=collection,
                          client=client,
                          pickle_protocol=pickle_protocol,
@@ -21,8 +20,10 @@ def add_job(self, job):
             self.collection.insert_one({
                 '_id': job.id,
                 'trigger': job.trigger.__str__(),
+                'codepack': job.kwargs['snapshot']['id'],
+                'snapshot': job.kwargs['snapshot']['serial_number'],
+                'last_run_time': datetime.now(timezone.utc).timestamp(),
                 'next_run_time': utc_timestamp,
-                'next_run_datetime': datetime.fromtimestamp(utc_timestamp),
                 'job_state': Binary(pickle.dumps(job.__getstate__(), self.pickle_protocol))
             })
         except DuplicateKeyError:
@@ -31,9 +32,8 @@ def add_job(self, job):
     def update_job(self, job):
         utc_timestamp = datetime_to_utc_timestamp(job.next_run_time)
         changes = {
-            'trigger': job.trigger.__str__(),
+            'last_run_time': datetime.now(timezone.utc).timestamp(),
             'next_run_time': utc_timestamp,
-            'next_run_datetime': datetime.fromtimestamp(utc_timestamp),
             'job_state': Binary(pickle.dumps(job.__getstate__(), self.pickle_protocol))
         }
         result = self.collection.update_one({'_id': job.id}, {'$set': changes})
diff --git a/codepack/scheduler/jobstores/__init__.py b/codepack/scheduler/jobstores/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/codepack/scheduler/mongo_scheduler.py b/codepack/scheduler/mongo_scheduler.py
index b247371..20d2a20 100644
--- a/codepack/scheduler/mongo_scheduler.py
+++ b/codepack/scheduler/mongo_scheduler.py
@@ -1,56 +1,13 @@
-from apscheduler.schedulers.background import BackgroundScheduler
-from codepack.scheduler.jobstores.mongodb import MongoDBJobStore
+from codepack.scheduler import Scheduler
+from codepack.scheduler.jobstore import MongoJobStore
 from codepack import CodePack
-from codepack.service import MongoDBService
+from codepack.storage import MongoStorage
 
 
-class MongoScheduler(MongoDBService):
-    def __init__(self, db=None, collection=None, config=None, ssh_config=None, mongodb=None, offline=False, **kwargs):
-        super().__init__(db=db, collection=collection,
-                         config=config, ssh_config=ssh_config, mongodb=mongodb, offline=offline)
-        if self.mongodb:
-            self.jobstores = {
-                'mongo': MongoDBJobStore(database=self.db,
-                                         collection=self.collection,
-                                         client=self.mongodb.client)
-            }
-            self.scheduler = BackgroundScheduler(jobstores=self.jobstores, **kwargs)
-        else:
-            self.scheduler = BackgroundScheduler(**kwargs)
-        self.scheduler.start()
+class MongoScheduler(MongoStorage, Scheduler):
+    def __init__(self, mongodb=None, db=None, collection=None, blocking=False, *args, **kwargs):
+        MongoStorage.__init__(self, obj=CodePack, mongodb=mongodb, db=db, collection=collection, *args, **kwargs)
+        Scheduler.__init__(self, blocking=blocking)
 
-    def __del__(self):
-        self.shutdown()
-
-    def __exit__(self, exc_type, exc_val, exc_tb):
-        self.shutdown()
-
-    def shutdown(self):
-        self.scheduler.shutdown()
-        self.mongodb.close()
-
-    def add_job(self, func, job_id, trigger, jobstore='mongo', **kwargs):
-        return self.scheduler.add_job(func=func, id=job_id, trigger=trigger, jobstore=jobstore, **kwargs)
-
-    def add_codepack(self, codepack, trigger, job_id=None, arg_dict=None, jobstore='mongo', **kwargs):
-        if arg_dict is None:
-            arg_dict = codepack.make_arg_dict()
-        if job_id is None:
-            job_id = codepack.id
-        ret = self.add_job(self.run_codepack_dict, job_id=job_id, trigger=trigger, jobstore=jobstore,
-                           kwargs={'codepack_dict': codepack.to_dict(), 'arg_dict': arg_dict}, **kwargs)
-        if hasattr(self, 'mongodb'):
-            self.mongodb[self.db][self.collection].update_one({'_id': job_id},
-                                                              {'$set': {'codepack': codepack.id, 'arg_dict': arg_dict}})
-        return ret
-
-    def add_codepack_from_db(self, id, db, collection, trigger, config, ssh_config=None, job_id=None, arg_dict=None, jobstore='mongo', **kwargs):
-        codepack = CodePack.from_db(id=id, db=db, collection=collection, config=config, ssh_config=ssh_config)
-        return self.add_codepack(codepack=codepack, trigger=trigger, job_id=job_id, arg_dict=arg_dict, jobstore=jobstore, **kwargs)
-
-    def remove_job(self, job_id, **kwargs):
-        return self.scheduler.remove_job(job_id=job_id, **kwargs)
-
-    @staticmethod
-    def run_codepack_dict(codepack_dict, arg_dict=None):
-        return CodePack.from_dict(codepack_dict)(arg_dict=arg_dict, lazy=False)
+    def get_jobstore(self):
+        return MongoJobStore(db=self.db, collection=self.collection, client=self.mongodb.session)
diff --git a/codepack/scheduler/scheduler.py b/codepack/scheduler/scheduler.py
new file mode 100644
index 0000000..f212031
--- /dev/null
+++ b/codepack/scheduler/scheduler.py
@@ -0,0 +1,54 @@
+import abc
+from apscheduler.schedulers.background import BackgroundScheduler, BlockingScheduler
+from codepack import CodePack
+from codepack.snapshot import CodePackSnapshot
+
+
+class Scheduler(metaclass=abc.ABCMeta):
+    def __init__(self, blocking=False):
+        self.scheduler = None
+        self.jobstores = dict()
+        self.jobstores['codepack'] = self.get_jobstore()
+        self.init_scheduler(blocking=blocking)
+
+    def start(self):
+        try:
+            self.scheduler.start()
+        except KeyboardInterrupt:
+            self.stop()
+
+    def init_scheduler(self, blocking=False, **kwargs):
+        if blocking:
+            self.scheduler = BlockingScheduler(jobstores=self.jobstores, **kwargs)
+        else:
+            self.scheduler = BackgroundScheduler(jobstores=self.jobstores, **kwargs)
+
+    @abc.abstractmethod
+    def get_jobstore(self):
+        """get jobstore"""
+
+    def stop(self):
+        self.scheduler.shutdown()
+
+    def add_job(self, func, job_id, trigger, **kwargs):
+        return self.scheduler.add_job(func=func, id=job_id, trigger=trigger, jobstore='codepack', **kwargs)
+
+    def add_codepack(self, codepack, trigger, job_id=None, argpack=None, **kwargs):
+        if job_id is None:
+            job_id = codepack.id
+        snapshot = codepack.to_snapshot(argpack=argpack)
+        ret = self.add_job(self.run_codepack, job_id=job_id, trigger=trigger, kwargs={'snapshot': snapshot.to_dict()}, **kwargs)
+        return ret
+
+    def remove_job(self, job_id, **kwargs):
+        return self.scheduler.remove_job(job_id=job_id, **kwargs)
+
+    @staticmethod
+    def run_codepack(snapshot):
+        if isinstance(snapshot, dict):
+            codepack = CodePack.from_snapshot(CodePackSnapshot.from_dict(snapshot))
+        elif isinstance(snapshot, CodePackSnapshot):
+            codepack = CodePack.from_snapshot(snapshot)
+        else:
+            raise TypeError(type(snapshot))
+        return codepack(argpack=snapshot['argpack'])
diff --git a/codepack/service/__init__.py b/codepack/service/__init__.py
index 1c0b9d4..ad24262 100644
--- a/codepack/service/__init__.py
+++ b/codepack/service/__init__.py
@@ -1,82 +1,4 @@
-from codepack.service.delivery_service import MemoryDeliveryService, FileDeliveryService, MongoDeliveryService
-from codepack.service.state_manager import MemoryStateManager, FileStateManager, MongoStateManager
-from codepack.service.storage_service import MemoryStorageService, FileStorageService, MongoStorageService
-from codepack.service.mongodb_service import MongoDBService
-from enum import Enum
-from codepack.utils import Singleton
-from codepack.utils.config import get_default_service_config
-
-
-class DeliveryServiceAlias(Enum):
-    MEMORY = MemoryDeliveryService
-    FILE = FileDeliveryService
-    MONGODB = MongoDeliveryService
-
-
-class StateManagerAlias(Enum):
-    MEMORY = MemoryStateManager
-    FILE = FileStateManager
-    MONGODB = MongoStateManager
-
-
-class StorageServiceAlias(Enum):
-    MEMORY = MemoryStorageService
-    FILE = FileStorageService
-    MONGODB = MongoStorageService
-
-
-class DefaultServicePack(Singleton):
-    delivery_service = None
-    state_manager = None
-    code_storage_service = None
-    codepack_storage_service = None
-
-    @classmethod
-    def init(cls):
-        cls.delivery_service = None
-        cls.state_manager = None
-        cls.code_storage_service = None
-        cls.codepack_storage_service = None
-
-    @classmethod
-    def get_default_delivery_service(cls, config_path=None, *args, **kwargs):
-        if not cls.delivery_service:
-            config = get_default_service_config(section='cache', config_path=config_path)
-            cls.delivery_service = get_delivery_service(*args, **config, **kwargs)
-        return cls.delivery_service
-
-    @classmethod
-    def get_default_state_manager(cls, config_path=None, *args, **kwargs):
-        if not cls.state_manager:
-            config = get_default_service_config(section='state', config_path=config_path)
-            cls.state_manager = get_state_manager(*args, **config, **kwargs)
-        return cls.state_manager
-
-    @classmethod
-    def get_default_code_storage_service(cls, obj, config_path=None, *args, **kwargs):
-        if not cls.code_storage_service:
-            config = get_default_service_config(section='code', config_path=config_path)
-            cls.code_storage_service = get_storage_service(obj=obj, *args, **config, **kwargs)
-        return cls.code_storage_service
-
-    @classmethod
-    def get_default_codepack_storage_service(cls, obj, config_path=None, *args, **kwargs):
-        if not cls.codepack_storage_service:
-            config = get_default_service_config(section='codepack', config_path=config_path)
-            cls.code_storage_service = get_storage_service(obj=obj, *args, **config, **kwargs)
-        return cls.code_storage_service
-
-
-def get_delivery_service(source, *args, **kwargs):
-    cls = DeliveryServiceAlias[source].value
-    return cls(*args, **kwargs)
-
-
-def get_state_manager(source, *args, **kwargs):
-    cls = StateManagerAlias[source].value
-    return cls(*args, **kwargs)
-
-
-def get_storage_service(source, *args, **kwargs):
-    cls = StorageServiceAlias[source].value
-    return cls(*args, **kwargs)
+from codepack.service.delivery_service import MemoryDeliveryService, FileDeliveryService, MongoDeliveryService, DeliveryServiceAlias
+from codepack.service.storage_service import MemoryStorageService, FileStorageService, MongoStorageService, StorageServiceAlias
+from codepack.service.snapshot_service import MemorySnapshotService, FileSnapshotService, MongoSnapshotService, SnapshotServiceAlias
+from codepack.service.default_service import DefaultService
diff --git a/codepack/service/abc.py b/codepack/service/abc.py
deleted file mode 100644
index b439c00..0000000
--- a/codepack/service/abc.py
+++ /dev/null
@@ -1,53 +0,0 @@
-import abc
-
-
-class Service(metaclass=abc.ABCMeta):
-    @abc.abstractmethod
-    def check(self, *args, **kwargs):
-        """check if exists"""
-
-
-class DeliveryService(Service, metaclass=abc.ABCMeta):
-    @abc.abstractmethod
-    def send(self, sender, invoice_number, item=None, send_time=None):
-        """send item"""
-
-    @abc.abstractmethod
-    def receive(self, invoice_number):
-        """receive item"""
-
-    @abc.abstractmethod
-    def cancel(self, invoice_number):
-        """cancel delivery"""
-
-
-class StateManager(Service, metaclass=abc.ABCMeta):
-    @abc.abstractmethod
-    def set(self, id, serial_number, state, update_time=None, args=None, kwargs=None, dependency=None):
-        """set state"""
-
-    @abc.abstractmethod
-    def get(self, serial_number):
-        """get state"""
-
-    @abc.abstractmethod
-    def remove(self, serial_number):
-        """remove state"""
-
-    @abc.abstractmethod
-    def search(self, state):
-        """search by state"""
-
-
-class StorageService(Service, metaclass=abc.ABCMeta):
-    @abc.abstractmethod
-    def save(self, obj):
-        """save object"""
-
-    @abc.abstractmethod
-    def load(self, id):
-        """load object with given id"""
-
-    @abc.abstractmethod
-    def remove(self, id):
-        """remove object with given id"""
diff --git a/codepack/service/default_service.py b/codepack/service/default_service.py
new file mode 100644
index 0000000..84cac00
--- /dev/null
+++ b/codepack/service/default_service.py
@@ -0,0 +1,73 @@
+from codepack.service.delivery_service import DeliveryServiceAlias
+from codepack.service.storage_service import StorageServiceAlias
+from codepack.service.snapshot_service import SnapshotServiceAlias
+from codepack.utils import Singleton
+from codepack.snapshot import CodeSnapshot, CodePackSnapshot
+from codepack.utils.delivery import Delivery
+from codepack.utils.config import get_default_service_config
+
+
+class DefaultService(Singleton):
+    delivery_service = None
+    code_storage_service = None
+    code_snapshot_service = None
+    codepack_storage_service = None
+    codepack_snapshot_service = None
+
+    @classmethod
+    def init(cls):
+        cls.delivery_service = None
+        cls.code_storage_service = None
+        cls.code_snapshot_service = None
+        cls.codepack_storage_service = None
+        cls.codepack_snapshot_service = None
+
+    @staticmethod
+    def get_delivery_service(source, *args, **kwargs):
+        cls = DeliveryServiceAlias[source].value
+        return cls(*args, **kwargs)
+
+    @staticmethod
+    def get_snapshot_service(source, *args, **kwargs):
+        cls = SnapshotServiceAlias[source].value
+        return cls(*args, **kwargs)
+
+    @staticmethod
+    def get_storage_service(source, *args, **kwargs):
+        cls = StorageServiceAlias[source].value
+        return cls(*args, **kwargs)
+
+    @classmethod
+    def get_default_delivery_service(cls, config_path=None):
+        if not cls.delivery_service:
+            config = get_default_service_config(section='cache', config_path=config_path)
+            cls.delivery_service = cls.get_delivery_service(obj=Delivery, **config)
+        return cls.delivery_service
+
+    @classmethod
+    def get_default_code_storage_service(cls, obj, config_path=None):
+        if not cls.code_storage_service:
+            config = get_default_service_config(section='code_storage', config_path=config_path)
+            cls.code_storage_service = cls.get_storage_service(obj=obj, **config)
+        return cls.code_storage_service
+
+    @classmethod
+    def get_default_code_snapshot_service(cls, config_path=None):
+        if not cls.code_snapshot_service:
+            config = get_default_service_config(section='code_snapshot', config_path=config_path)
+            cls.code_snapshot_service = cls.get_snapshot_service(obj=CodeSnapshot, **config)
+        return cls.code_snapshot_service
+
+    @classmethod
+    def get_default_codepack_storage_service(cls, obj, config_path=None):
+        if not cls.codepack_storage_service:
+            config = get_default_service_config(section='codepack_storage', config_path=config_path)
+            cls.codepack_storage_service = cls.get_storage_service(obj=obj, **config)
+        return cls.codepack_storage_service
+
+    @classmethod
+    def get_default_codepack_snapshot_service(cls, config_path=None):
+        if not cls.codepack_snapshot_service:
+            config = get_default_service_config(section='codepack_snapshot', config_path=config_path)
+            cls.codepack_snapshot_service = cls.get_snapshot_service(obj=CodePackSnapshot, **config)
+        return cls.codepack_snapshot_service
diff --git a/codepack/service/delivery_service.py b/codepack/service/delivery_service.py
deleted file mode 100644
index f3dcc6e..0000000
--- a/codepack/service/delivery_service.py
+++ /dev/null
@@ -1,113 +0,0 @@
-from codepack.service.mongodb_service import MongoDBService
-from codepack.service.abc import DeliveryService
-from codepack.utils.order import Order
-from collections.abc import Iterable
-from codepack.utils import Singleton
-from datetime import datetime
-import json
-import os
-
-
-class MemoryDeliveryService(DeliveryService, Singleton):
-    def __init__(self):
-        super().__init__()
-        if not hasattr(self, 'deliveries'):
-            self.deliveries = dict()
-
-    def init(self):
-        self.deliveries = dict()
-
-    def send(self, sender, invoice_number, item=None, send_time=None):
-        self.deliveries[invoice_number] = Order(sender=sender, invoice_number=invoice_number, item=item, send_time=send_time)
-
-    def receive(self, invoice_number):
-        return self.deliveries[invoice_number].receive()
-
-    def check(self, invoice_number):
-        if isinstance(invoice_number, str):
-            if invoice_number in self.deliveries:
-                d = self.deliveries[invoice_number].to_dict()
-                d.pop('item', None)
-                return d
-            else:
-                return None
-        elif isinstance(invoice_number, Iterable):
-            ret = list()
-            for i in invoice_number:
-                tmp = self.check(i)
-                if tmp:
-                    ret.append(tmp)
-            return ret
-        else:
-            raise TypeError(type(invoice_number))  # pragma: no cover
-
-    def cancel(self, invoice_number):
-        self.deliveries.pop(invoice_number, None)
-
-
-class FileDeliveryService(DeliveryService):
-    def __init__(self, path='./'):
-        super().__init__()
-        self.path = path
-
-    def send(self, sender, invoice_number, item=None, send_time=None, path=None):
-        Order(sender=sender, invoice_number=invoice_number, item=item, send_time=send_time)\
-            .to_file(path=Order.get_path(serial_number=invoice_number, path=path if path else self.path))
-
-    def receive(self, invoice_number, path=None):
-        return Order.from_file(path=Order.get_path(serial_number=invoice_number, path=path if path else self.path)).receive()
-
-    def check(self, invoice_number, path=None):
-        if isinstance(invoice_number, str):
-            d = None
-            try:
-                d = Order.from_file(path=Order.get_path(serial_number=invoice_number, path=path if path else self.path)).to_dict()
-                d.pop('item', None)
-            finally:
-                return d
-        elif isinstance(invoice_number, Iterable):
-            ret = list()
-            for i in invoice_number:
-                tmp = self.check(i)
-                if tmp:
-                    ret.append(tmp)
-            return ret
-        else:
-            raise TypeError(type(invoice_number))  # pragma: no cover
-
-    def cancel(self, invoice_number, path=None):
-        os.remove(Order.get_path(serial_number=invoice_number, path=path if path else self.path))
-
-
-class MongoDeliveryService(DeliveryService, MongoDBService):
-    def __init__(self, db=None, collection=None,
-                 mongodb=None, *args, **kwargs):
-        MongoDBService.__init__(self, db=db, collection=collection,
-                                mongodb=mongodb, *args, **kwargs)
-        DeliveryService.__init__(self)
-
-    def send(self, sender, invoice_number, item=None, send_time=None, db=None, collection=None):
-        if not send_time:
-            send_time = datetime.now().timestamp()
-        d = {'sender': sender, 'send_time': send_time, 'item': json.dumps(item)}
-        self.mongodb[db if db else self.db][collection if collection else self.collection]\
-            .update_one({'_id': invoice_number}, {'$set': d}, upsert=True)
-
-    def receive(self, invoice_number, db=None, collection=None):
-        ret = self.mongodb[db if db else self.db][collection if collection else self.collection]\
-            .find_one({'_id': invoice_number})
-        return json.loads(ret['item'])
-
-    def check(self, invoice_number, db=None, collection=None):
-        if isinstance(invoice_number, str):
-            return self.mongodb[db if db else self.db][collection if collection else self.collection]\
-                .find_one({'_id': invoice_number}, projection={'item': 0})
-        elif isinstance(invoice_number, Iterable):
-            return list(self.mongodb[db if db else self.db][collection if collection else self.collection]
-                        .find({'_id': {'$in': invoice_number}}, projection={'item': 0}))
-        else:
-            raise TypeError(type(invoice_number))  # pragma: no cover
-
-    def cancel(self, invoice_number, db=None, collection=None):
-        self.mongodb[db if db else self.db][collection if collection else self.collection]\
-            .delete_one({'_id': invoice_number})
diff --git a/codepack/service/delivery_service/__init__.py b/codepack/service/delivery_service/__init__.py
new file mode 100644
index 0000000..d4c0589
--- /dev/null
+++ b/codepack/service/delivery_service/__init__.py
@@ -0,0 +1,12 @@
+from codepack.service.delivery_service.delivery_service import DeliveryService
+from codepack.service.delivery_service.memory_delivery_service import MemoryDeliveryService
+from codepack.service.delivery_service.file_delivery_service import FileDeliveryService
+from codepack.service.delivery_service.mongo_delivery_service import MongoDeliveryService
+
+from enum import Enum
+
+
+class DeliveryServiceAlias(Enum):
+    MEMORY = MemoryDeliveryService
+    FILE = FileDeliveryService
+    MONGODB = MongoDeliveryService
diff --git a/codepack/service/delivery_service/delivery_service.py b/codepack/service/delivery_service/delivery_service.py
new file mode 100644
index 0000000..7382ea8
--- /dev/null
+++ b/codepack/service/delivery_service/delivery_service.py
@@ -0,0 +1,23 @@
+import abc
+from codepack.service.service import Service
+
+
+class DeliveryService(Service, metaclass=abc.ABCMeta):
+    def __init__(self):
+        super().__init__()
+
+    @abc.abstractmethod
+    def send(self, id, serial_number, item=None, timestamp=None):
+        """send item"""
+
+    @abc.abstractmethod
+    def receive(self, serial_number):
+        """receive item"""
+
+    @abc.abstractmethod
+    def cancel(self, serial_number):
+        """cancel delivery"""
+
+    @abc.abstractmethod
+    def check(self, serial_number):
+        """check arrival of delivery"""
diff --git a/codepack/service/delivery_service/file_delivery_service.py b/codepack/service/delivery_service/file_delivery_service.py
new file mode 100644
index 0000000..606df28
--- /dev/null
+++ b/codepack/service/delivery_service/file_delivery_service.py
@@ -0,0 +1,37 @@
+import os
+from codepack.storage import FileStorage
+from codepack.service.delivery_service import DeliveryService
+from collections.abc import Iterable
+
+
+class FileDeliveryService(DeliveryService, FileStorage):
+    def __init__(self, obj=None, path='./'):
+        FileStorage.__init__(self, obj=obj, path=path)
+
+    def send(self, id, serial_number, item=None, timestamp=None):
+        self.obj(id=id, serial_number=serial_number, item=item, timestamp=timestamp)\
+            .to_file(path=self.obj.get_path(serial_number=serial_number, path=self.path))
+
+    def receive(self, serial_number):
+        return self.obj.from_file(path=self.obj.get_path(serial_number=serial_number, path=self.path)).receive()
+
+    def check(self, serial_number):
+        if isinstance(serial_number, str):
+            d = None
+            try:
+                d = self.obj.from_file(path=self.obj.get_path(serial_number=serial_number, path=self.path)).to_dict()
+                d.pop('item', None)
+            finally:
+                return d
+        elif isinstance(serial_number, Iterable):
+            ret = list()
+            for i in serial_number:
+                tmp = self.check(i)
+                if tmp:
+                    ret.append(tmp)
+            return ret
+        else:
+            raise TypeError(type(serial_number))  # pragma: no cover
+
+    def cancel(self, serial_number):
+        os.remove(self.obj.get_path(serial_number=serial_number, path=self.path))
diff --git a/codepack/service/delivery_service/memory_delivery_service.py b/codepack/service/delivery_service/memory_delivery_service.py
new file mode 100644
index 0000000..98ad8e5
--- /dev/null
+++ b/codepack/service/delivery_service/memory_delivery_service.py
@@ -0,0 +1,35 @@
+from codepack.storage import MemoryStorage
+from codepack.service.delivery_service import DeliveryService
+from collections.abc import Iterable
+
+
+class MemoryDeliveryService(DeliveryService, MemoryStorage):
+    def __init__(self, obj=None):
+        MemoryStorage.__init__(self, obj=obj)
+
+    def send(self, id, serial_number, item=None, timestamp=None):
+        self.memory[serial_number] = self.obj(id=id, serial_number=serial_number, item=item, timestamp=timestamp)
+
+    def receive(self, serial_number):
+        return self.memory[serial_number].receive()
+
+    def check(self, serial_number):
+        if isinstance(serial_number, str):
+            if serial_number in self.memory:
+                d = self.memory[serial_number].to_dict()
+                d.pop('item', None)
+                return d
+            else:
+                return None
+        elif isinstance(serial_number, Iterable):
+            ret = list()
+            for i in serial_number:
+                tmp = self.check(i)
+                if tmp:
+                    ret.append(tmp)
+            return ret
+        else:
+            raise TypeError(type(serial_number))  # pragma: no cover
+
+    def cancel(self, serial_number):
+        self.memory.pop(serial_number, None)
diff --git a/codepack/service/delivery_service/mongo_delivery_service.py b/codepack/service/delivery_service/mongo_delivery_service.py
new file mode 100644
index 0000000..2290ef1
--- /dev/null
+++ b/codepack/service/delivery_service/mongo_delivery_service.py
@@ -0,0 +1,29 @@
+from codepack.storage import MongoStorage
+from codepack.service.delivery_service import DeliveryService
+from collections.abc import Iterable
+import json
+
+
+class MongoDeliveryService(DeliveryService, MongoStorage):
+    def __init__(self, obj=None, db=None, collection=None, mongodb=None, *args, **kwargs):
+        MongoStorage.__init__(self, obj=obj, db=db, collection=collection, mongodb=mongodb, *args, **kwargs)
+
+    def send(self, id, serial_number, item=None, timestamp=None):
+        d = self.obj(id=id, serial_number=serial_number, item=item, timestamp=timestamp).to_dict()
+        d.pop('_id', None)
+        self.mongodb[self.db][self.collection].update_one({'_id': serial_number}, {'$set': d}, upsert=True)
+
+    def receive(self, serial_number):
+        ret = self.mongodb[self.db][self.collection].find_one({'_id': serial_number})
+        return json.loads(ret['item'])
+
+    def check(self, serial_number):
+        if isinstance(serial_number, str):
+            return self.mongodb[self.db][self.collection].find_one({'_id': serial_number}, projection={'item': 0})
+        elif isinstance(serial_number, Iterable):
+            return list(self.mongodb[self.db][self.collection].find({'_id': {'$in': serial_number}}, projection={'item': 0}))
+        else:
+            raise TypeError(type(serial_number))  # pragma: no cover
+
+    def cancel(self, serial_number):
+        self.mongodb[self.db][self.collection].delete_one({'_id': serial_number})
diff --git a/codepack/service/mongodb_service.py b/codepack/service/mongodb_service.py
deleted file mode 100644
index 21df0cc..0000000
--- a/codepack/service/mongodb_service.py
+++ /dev/null
@@ -1,64 +0,0 @@
-from codepack.interface import MongoDB
-from codepack.utils.config import get_config, get_config_assertion_error_message
-import os
-
-
-class MongoDBService:
-    def __init__(self, mongodb=None, config_path=None, section=None, conn_config=None, ssh_config=None,
-                 db=None, collection=None, **kwargs):
-        self.mongodb = None
-        self.db = None
-        self.collection = None
-        self.new_connection = None
-        self.link_to_mongodb(mongodb=mongodb, config_path=config_path,
-                             section=section,
-                             conn_config=conn_config, ssh_config=ssh_config,
-                             db=db, collection=collection, **kwargs)
-
-    def link_to_mongodb(self, mongodb=None, config_path=None, section=None, conn_config=None, ssh_config=None,
-                        db=None, collection=None, **kwargs):
-        mongodb, db, collection, new_connection = self.get_mongodb(mongodb=mongodb, config_path=config_path,
-                                                                   section=section,
-                                                                   conn_config=conn_config, ssh_config=ssh_config,
-                                                                   db=db, collection=collection, **kwargs)
-        self.mongodb = mongodb
-        self.db = db
-        self.collection = collection
-        self.new_connection = new_connection
-
-    @classmethod
-    def get_db_info(cls, section=None, config_path=None, conn_config=None, db=None, collection=None):
-        if not config_path:
-            config_path = os.environ.get('CODEPACK_CONFIG_PATH', None)
-        if config_path:
-            if not conn_config or not db or not collection:
-                config = get_config(config_path, section=section)
-                if not conn_config:
-                    if config['source'] not in ['mongodb']:
-                        raise NotImplementedError("'%s' is unknown data source")
-                    conn_config_path = get_config(config_path, section='conn')['path']
-                    conn_config = get_config(conn_config_path, section=config['source'])
-                if not db:
-                    db = config['db']
-                if not collection:
-                    collection = config['collection']
-        else:
-            for k, v in {'db': db, 'collection': collection}.items():
-                if not v:
-                    raise AssertionError(get_config_assertion_error_message(k))
-        return conn_config, db, collection
-
-    @classmethod
-    def get_mongodb(cls, mongodb=None, config_path=None, section=None, conn_config=None, ssh_config=None,
-                    db=None, collection=None, **kwargs):
-        new_connection = False
-        if not mongodb:
-            conn_config, db, collection = cls.get_db_info(config_path=config_path, section=section,
-                                                          conn_config=conn_config, db=db, collection=collection)
-            if conn_config:
-                mongodb = MongoDB(config=conn_config, ssh_config=ssh_config, **kwargs)
-                new_connection = True
-        else:
-            _, db, collection = cls.get_db_info(config_path=config_path, section=section,
-                                                conn_config=mongodb.config, db=db, collection=collection)
-        return mongodb, db, collection, new_connection
diff --git a/codepack/service/service.py b/codepack/service/service.py
new file mode 100644
index 0000000..8b26dc4
--- /dev/null
+++ b/codepack/service/service.py
@@ -0,0 +1,5 @@
+import abc
+
+
+class Service(metaclass=abc.ABCMeta):
+    pass
diff --git a/codepack/service/snapshot_service/__init__.py b/codepack/service/snapshot_service/__init__.py
new file mode 100644
index 0000000..9bc7e6e
--- /dev/null
+++ b/codepack/service/snapshot_service/__init__.py
@@ -0,0 +1,11 @@
+from codepack.service.snapshot_service.snapshot_service import SnapshotService
+from codepack.service.snapshot_service.memory_snapshot_service import MemorySnapshotService
+from codepack.service.snapshot_service.file_snapshot_service import FileSnapshotService
+from codepack.service.snapshot_service.mongo_snapshot_service import MongoSnapshotService
+from enum import Enum
+
+
+class SnapshotServiceAlias(Enum):
+    MEMORY = MemorySnapshotService
+    FILE = FileSnapshotService
+    MONGODB = MongoSnapshotService
diff --git a/codepack/service/snapshot_service/file_snapshot_service.py b/codepack/service/snapshot_service/file_snapshot_service.py
new file mode 100644
index 0000000..3644b65
--- /dev/null
+++ b/codepack/service/snapshot_service/file_snapshot_service.py
@@ -0,0 +1,60 @@
+import os
+from codepack.storage import FileStorage
+from codepack.service.snapshot_service import SnapshotService
+from collections.abc import Iterable
+from glob import glob
+
+
+class FileSnapshotService(SnapshotService, FileStorage):
+    def __init__(self, obj=None, path='./'):
+        FileStorage.__init__(self, obj=obj, path=path)
+
+    def save(self, snapshot):
+        if isinstance(snapshot, self.obj):
+            d = self.load(snapshot.serial_number)
+            if d:
+                existing_snapshot = self.obj.from_dict(d)
+                for key, value in existing_snapshot.diff(snapshot).items():
+                    existing_snapshot[key] = value
+                    existing_snapshot.to_file(self.obj.get_path(serial_number=existing_snapshot.serial_number, path=self.path))
+            else:
+                snapshot.to_file(self.obj.get_path(serial_number=snapshot.serial_number, path=self.path))
+        else:
+            raise TypeError(type(snapshot))
+
+    def load(self, serial_number, projection=None):
+        if isinstance(serial_number, str):
+            ret = None
+            try:
+                d = self.obj.from_file(self.obj.get_path(serial_number=serial_number, path=self.path)).to_dict()
+                if projection:
+                    ret = {k: d[k] for k in set(projection).union({'serial_number'})}
+                else:
+                    ret = d
+            finally:
+                return ret
+        elif isinstance(serial_number, Iterable):
+            ret = list()
+            for s in serial_number:
+                tmp = self.load(serial_number=s, projection=projection)
+                if tmp:
+                    ret.append(tmp)
+            return ret
+        else:
+            raise TypeError(type(serial_number))  # pragma: no cover
+
+    def remove(self, serial_number):
+        os.remove(self.obj.get_path(serial_number=serial_number, path=self.path))
+
+    def search(self, key, value, projection=None):
+        ret = list()
+        for filename in glob(self.path + '*.json'):
+            snapshot = self.obj.from_file(filename)
+            if snapshot[key] != value:
+                continue
+            d = snapshot.to_dict()
+            if projection:
+                ret.append({k: d[k] for k in set(projection).union({'serial_number'})})
+            else:
+                ret.append(d)
+        return ret
diff --git a/codepack/service/snapshot_service/memory_snapshot_service.py b/codepack/service/snapshot_service/memory_snapshot_service.py
new file mode 100644
index 0000000..1ce1321
--- /dev/null
+++ b/codepack/service/snapshot_service/memory_snapshot_service.py
@@ -0,0 +1,54 @@
+from codepack.storage import MemoryStorage
+from codepack.service.snapshot_service import SnapshotService
+from collections.abc import Iterable
+
+
+class MemorySnapshotService(SnapshotService, MemoryStorage):
+    def __init__(self, obj=None):
+        MemoryStorage.__init__(self, obj=obj)
+
+    def save(self, snapshot):
+        if isinstance(snapshot, self.obj):
+            d = self.load(snapshot.serial_number)
+            if d:
+                for key, value in self.obj.from_dict(d).diff(snapshot).items():
+                    self.memory[snapshot.serial_number][key] = value
+            else:
+                self.memory[snapshot.serial_number] = snapshot
+        else:
+            raise TypeError(type(snapshot))
+
+    def load(self, serial_number, projection=None):
+        if isinstance(serial_number, str):
+            if serial_number in self.memory:
+                d = self.memory[serial_number].to_dict()
+                if projection:
+                    return {k: d[k] for k in set(projection).union({'serial_number'})}
+                else:
+                    return d
+            else:
+                return None
+        elif isinstance(serial_number, Iterable):
+            ret = list()
+            for s in serial_number:
+                tmp = self.load(serial_number=s, projection=projection)
+                if tmp:
+                    ret.append(tmp)
+            return ret
+        else:
+            raise TypeError(type(serial_number))  # pragma: no cover
+
+    def remove(self, serial_number):
+        self.memory.pop(serial_number, None)
+
+    def search(self, key, value, projection=None):
+        ret = list()
+        for snapshot in self.memory.values():
+            if snapshot[key] != value:
+                continue
+            d = snapshot.to_dict()
+            if projection:
+                ret.append({k: d[k] for k in set(projection).union({'serial_number'})})
+            else:
+                ret.append(d)
+        return ret
diff --git a/codepack/service/snapshot_service/mongo_snapshot_service.py b/codepack/service/snapshot_service/mongo_snapshot_service.py
new file mode 100644
index 0000000..65d3cf9
--- /dev/null
+++ b/codepack/service/snapshot_service/mongo_snapshot_service.py
@@ -0,0 +1,52 @@
+from codepack.storage import MongoStorage
+from codepack.service.snapshot_service import SnapshotService
+from collections.abc import Iterable
+from codepack.utils.state import State
+
+
+class MongoSnapshotService(SnapshotService, MongoStorage):
+    def __init__(self, obj=None, mongodb=None, db=None, collection=None, *args, **kwargs):
+        MongoStorage.__init__(self, obj=obj, mongodb=mongodb, db=db, collection=collection, *args, **kwargs)
+
+    def save(self, snapshot):
+        if isinstance(snapshot, self.obj):
+            d = self.load(snapshot.serial_number)
+            if d:
+                existing_snapshot = self.obj.from_dict(d)
+                diff = existing_snapshot.diff(snapshot)
+                for key, value in diff.items():
+                    existing_snapshot[key] = value
+                if 'state' in diff and isinstance(diff['state'], State):
+                    diff['state'] = diff['state'].name
+                self.mongodb[self.db][self.collection].update_one({'_id': existing_snapshot.serial_number},
+                                                                  {'$set': diff}, upsert=True)
+            else:
+                snapshot.to_db(mongodb=self.mongodb, db=self.db, collection=self.collection)
+        else:
+            raise TypeError(type(snapshot))
+
+    def load(self, serial_number, projection=None):
+        if projection:
+            _projection = {k: True for k in projection}
+            _projection['serial_number'] = True
+            _projection['_id'] = False
+        else:
+            _projection = projection
+        if isinstance(serial_number, str):
+            return self.mongodb[self.db][self.collection].find_one({'_id': serial_number}, projection=_projection)
+        elif isinstance(serial_number, Iterable):
+            return list(self.mongodb[self.db][self.collection].find({'_id': {'$in': serial_number}}, projection=_projection))
+        else:
+            raise TypeError(type(serial_number))  # pragma: no cover
+
+    def remove(self, serial_number):
+        self.mongodb[self.db][self.collection].delete_one({'_id': serial_number})
+
+    def search(self, key, value, projection=None):
+        if projection:
+            _projection = {k: True for k in projection}
+            _projection['serial_number'] = True
+            _projection['_id'] = False
+        else:
+            _projection = projection
+        return list(self.mongodb[self.db][self.collection].find({key: value}, projection=_projection))
diff --git a/codepack/service/snapshot_service/snapshot_service.py b/codepack/service/snapshot_service/snapshot_service.py
new file mode 100644
index 0000000..548bd16
--- /dev/null
+++ b/codepack/service/snapshot_service/snapshot_service.py
@@ -0,0 +1,23 @@
+import abc
+from codepack.service.service import Service
+
+
+class SnapshotService(Service, metaclass=abc.ABCMeta):
+    def __init__(self):
+        super().__init__()
+
+    @abc.abstractmethod
+    def save(self, snapshot):
+        """save snapshot"""
+
+    @abc.abstractmethod
+    def load(self, serial_number, projection=None):
+        """load snapshot"""
+
+    @abc.abstractmethod
+    def remove(self, serial_number):
+        """remove snapshot"""
+
+    @abc.abstractmethod
+    def search(self, key, value, projection=None):
+        """search by key and value"""
diff --git a/codepack/service/state_manager.py b/codepack/service/state_manager.py
deleted file mode 100644
index b29623a..0000000
--- a/codepack/service/state_manager.py
+++ /dev/null
@@ -1,159 +0,0 @@
-from codepack.service.abc import StateManager
-from codepack.service.mongodb_service import MongoDBService
-from codepack.utils.state import State, StateCode
-from collections.abc import Iterable
-from codepack.utils import Singleton
-import os
-from glob import glob
-
-
-class MemoryStateManager(StateManager, Singleton):
-    def __init__(self):
-        super().__init__()
-        if not hasattr(self, 'states'):
-            self.states = dict()
-
-    def init(self):
-        self.states = dict()
-
-    def set(self, id, serial_number, state, update_time=None, args=None, kwargs=None, dependency=None):
-        self.states[serial_number] = State(id=id, serial_number=serial_number,
-                                           state=state, update_time=update_time, args=args, kwargs=kwargs, dependency=dependency)
-
-    def get(self, serial_number):
-        if isinstance(serial_number, str):
-            ret = StateCode.UNKNOWN
-            try:
-                ret = self.states[serial_number].state
-            finally:
-                return ret
-        elif isinstance(serial_number, Iterable):
-            ret = list()
-            for s in serial_number:
-                tmp = self.get(s)
-                if tmp:
-                    ret.append(tmp)
-            return ret
-        else:
-            raise TypeError(type(serial_number))  # pragma: no cover
-
-    def check(self, serial_number):
-        if isinstance(serial_number, str):
-            if serial_number in self.states:
-                return self.states[serial_number].to_dict()
-            else:
-                return None
-        elif isinstance(serial_number, Iterable):
-            ret = list()
-            for s in serial_number:
-                tmp = self.check(s)
-                if tmp:
-                    ret.append(tmp)
-            return ret
-        else:
-            raise TypeError(type(serial_number))  # pragma: no cover
-
-    def remove(self, serial_number):
-        self.states.pop(serial_number, None)
-
-    def search(self, state):
-        return [s.to_dict() for s in self.states.values() if s.state == state]
-
-
-class FileStateManager(StateManager):
-    def __init__(self, path='./'):
-        super().__init__()
-        self.path = path
-
-    def set(self, id, serial_number, state, update_time=None, args=None, kwargs=None, dependency=None, path=None):
-        State(id=id, serial_number=serial_number, state=state, update_time=update_time, args=args, kwargs=kwargs, dependency=dependency)\
-              .to_file(path=State.get_path(serial_number=serial_number, path=path if path else self.path))
-
-    def get(self, serial_number, path=None):
-        if isinstance(serial_number, str):
-            ret = StateCode.UNKNOWN
-            try:
-                ret = State.from_file(path=State.get_path(serial_number=serial_number, path=path if path else self.path)).get()
-            finally:
-                return ret
-        elif isinstance(serial_number, Iterable):
-            ret = list()
-            for s in serial_number:
-                tmp = self.get(s)
-                if tmp:
-                    ret.append(tmp)
-            return ret
-        else:
-            raise TypeError(type(serial_number))  # pragma: no cover
-
-    def check(self, serial_number, path=None):
-        if isinstance(serial_number, str):
-            d = None
-            try:
-                d = State.from_file(path=State.get_path(serial_number=serial_number, path=path if path else self.path)).to_dict()
-            finally:
-                return d
-        elif isinstance(serial_number, Iterable):
-            ret = list()
-            for s in serial_number:
-                tmp = self.check(s)
-                if tmp:
-                    ret.append(tmp)
-            return ret
-        else:
-            raise TypeError(type(serial_number))  # pragma: no cover
-
-    def remove(self, serial_number, path=None):
-        os.remove(State.get_path(serial_number=serial_number, path=path if path else self.path))
-
-    def search(self, state, path=None):
-        ret = list()
-        dirname = path if path else self.path
-        for filename in glob(dirname + '*.json'):
-            d = State.from_file(path=filename).to_dict()
-            if d['state'] == state:
-                ret.append(d)
-        return ret
-
-
-class MongoStateManager(StateManager, MongoDBService):
-    def __init__(self, db=None, collection=None,
-                 mongodb=None, *args, **kwargs):
-        MongoDBService.__init__(self, db=db, collection=collection,
-                                mongodb=mongodb, *args, **kwargs)
-        StateManager.__init__(self)
-
-    def set(self, id, serial_number, state=None, update_time=None, args=None, kwargs=None, dependency=None, db=None, collection=None):
-        tmp = State(id=id, serial_number=serial_number, state=state, update_time=update_time, args=args, kwargs=kwargs, dependency=dependency).to_dict()
-        _id = tmp.pop('_id')
-        self.mongodb[db if db else self.db][collection if collection else self.collection]\
-            .update_one({'_id': _id}, {'$set': tmp}, upsert=True)
-
-    def get(self, serial_number, db=None, collection=None):
-        if isinstance(serial_number, str):
-            tmp = self.mongodb[db if db else self.db][collection if collection else self.collection]\
-                .find_one({'_id': serial_number})
-            return State.from_dict(tmp).state if tmp else StateCode.UNKNOWN
-        elif isinstance(serial_number, Iterable):
-            tmp = {x['_id']: x['state'] for x in self.mongodb[db if db else self.db][collection if collection else self.collection]
-                .find({'_id': {'$in': list(serial_number)}}, projection={'state': 1})}
-            return [tmp[s] for s in serial_number]
-        else:
-            raise TypeError(type(serial_number))  # pragma: no cover
-
-    def check(self, serial_number, db=None, collection=None):
-        if isinstance(serial_number, str):
-            return self.mongodb[db if db else self.db][collection if collection else self.collection]\
-                .find_one({'_id': serial_number})
-        elif isinstance(serial_number, Iterable):
-            return list(self.mongodb[db if db else self.db][collection if collection else self.collection]
-                        .find({'_id': {'$in': list(serial_number)}}))
-        else:
-            raise TypeError(type(serial_number))  # pragma: no cover
-
-    def remove(self, serial_number, db=None, collection=None):
-        self.mongodb[db if db else self.db][collection if collection else self.collection]\
-            .delete_one({'_id': serial_number})
-
-    def search(self, state, db=None, collection=None):
-        return list(self.mongodb[db if db else self.db][collection if collection else self.collection].find({'state': state}))
diff --git a/codepack/service/storage_service.py b/codepack/service/storage_service.py
deleted file mode 100644
index 491d1e9..0000000
--- a/codepack/service/storage_service.py
+++ /dev/null
@@ -1,127 +0,0 @@
-from codepack.service.abc import StorageService
-from codepack.service.mongodb_service import MongoDBService
-from collections.abc import Iterable
-from codepack.utils import Singleton
-import os
-
-
-class MemoryStorageService(StorageService, Singleton):
-    def __init__(self, obj=None):
-        super().__init__()
-        if not hasattr(self, 'storage'):
-            self.storage = dict()
-        self.obj = obj
-
-    def init(self):
-        self.storage = dict()
-
-    def save(self, obj):
-        self.storage[obj.id] = obj
-
-    def load(self, id):
-        return self.storage[id]
-
-    def remove(self, id):
-        self.storage.pop(id, None)
-
-    def check(self, id):
-        if isinstance(id, str):
-            if id in self.storage:
-                return {self.storage[id].id: True}
-            else:
-                return None
-        elif isinstance(id, Iterable):
-            ret = list()
-            for i in id:
-                tmp = self.check(i)
-                if tmp:
-                    ret.append(tmp)
-            return ret
-        else:
-            raise TypeError(type(id))  # pragma: no cover
-
-
-class FileStorageService(StorageService):
-    def __init__(self, obj, path='./'):
-        super().__init__()
-        self.obj = obj
-        self.path = path
-
-    @staticmethod
-    def get_path(id, path='./'):
-        return '%s%s.json' % (path, id)
-
-    def save(self, obj, path=None):
-        obj.to_file(self.get_path(id=obj.id, path=path if path else self.path))
-
-    def load(self, id, obj=None, path=None):
-        if obj is None:
-            obj = self.obj
-        return obj.from_file(path=self.get_path(id=id, path=path if path else self.path))
-
-    def check(self, id, obj=None, path=None):
-        if isinstance(id, str):
-            if obj is None:
-                obj = self.obj
-            d = None
-            try:
-                tmp = obj.from_file(path=self.get_path(id=id, path=path if path else self.path))
-                d = {tmp.id: True}
-            finally:
-                return d
-        elif isinstance(id, Iterable):
-            ret = list()
-            for i in id:
-                tmp = self.check(i)
-                if tmp:
-                    ret.append(tmp)
-            return ret
-        else:
-            raise TypeError(type(id))  # pragma: no cover
-
-    def remove(self, id, path=None):
-        os.remove(path=self.get_path(id=id, path=path if path else self.path))
-
-
-class MongoStorageService(StorageService, MongoDBService):
-    def __init__(self, obj, db=None, collection=None,
-                 mongodb=None, *args, **kwargs):
-        MongoDBService.__init__(self, db=db, collection=collection,
-                                mongodb=mongodb, *args, **kwargs)
-        StorageService.__init__(self)
-        self.obj = obj
-
-    def save(self, obj, db=None, collection=None):
-        obj.to_db(mongodb=self.mongodb,
-                  db=db if db else self.db, collection=collection if collection else self.collection)
-
-    def load(self, id, obj=None, db=None, collection=None):
-        if obj is None:
-            obj = self.obj
-        return obj.from_db(id=id, mongodb=self.mongodb,
-                           db=db if db else self.db, collection=collection if collection else self.collection)
-
-    def check(self, id, obj=None, db=None, collection=None):
-        if isinstance(id, str):
-            if obj is None:
-                obj = self.obj
-            d = None
-            try:
-                tmp = obj.from_db(id=id, mongodb=self.mongodb,
-                                  db=db if db else self.db, collection=collection if collection else self.collection)
-                d = {tmp.id: True}
-            finally:
-                return d
-        elif isinstance(id, Iterable):
-            ret = list()
-            for i in id:
-                tmp = self.check(i)
-                if tmp:
-                    ret.append(tmp)
-            return ret
-        else:
-            raise TypeError(type(id))  # pragma: no cover
-
-    def remove(self, id, db=None, collection=None):
-        self.mongodb[db if db else self.db][collection if collection else self.collection]\
-            .delete_one({'_id': id})
diff --git a/codepack/service/storage_service/__init__.py b/codepack/service/storage_service/__init__.py
new file mode 100644
index 0000000..2466e21
--- /dev/null
+++ b/codepack/service/storage_service/__init__.py
@@ -0,0 +1,11 @@
+from codepack.service.storage_service.storage_service import StorageService
+from codepack.service.storage_service.memory_storage_service import MemoryStorageService
+from codepack.service.storage_service.file_storage_service import FileStorageService
+from codepack.service.storage_service.mongo_storage_service import MongoStorageService
+from enum import Enum
+
+
+class StorageServiceAlias(Enum):
+    MEMORY = MemoryStorageService
+    FILE = FileStorageService
+    MONGODB = MongoStorageService
diff --git a/codepack/service/storage_service/file_storage_service.py b/codepack/service/storage_service/file_storage_service.py
new file mode 100644
index 0000000..43ee07d
--- /dev/null
+++ b/codepack/service/storage_service/file_storage_service.py
@@ -0,0 +1,38 @@
+import os
+from codepack.storage import FileStorage
+from codepack.service.storage_service import StorageService
+from collections.abc import Iterable
+
+
+class FileStorageService(StorageService, FileStorage):
+    def __init__(self, obj=None, path='./'):
+        FileStorage.__init__(self, obj=obj, path=path)
+
+    def save(self, item):
+        if isinstance(item, self.obj):
+            item.to_file(self.obj.get_path(serial_number=item.id, path=self.path))
+        else:
+            raise TypeError(type(item))
+
+    def load(self, id):
+        return self.obj.from_file(path=self.obj.get_path(serial_number=id, path=self.path))
+
+    def remove(self, id):
+        os.remove(path=self.obj.get_path(serial_number=id, path=self.path))
+
+    def check(self, id):
+        if isinstance(id, str):
+            ret = None
+            try:
+                ret = self.obj.from_file(path=self.obj.get_path(serial_number=id, path=self.path)).id
+            finally:
+                return ret
+        elif isinstance(id, Iterable):
+            ret = list()
+            for i in id:
+                tmp = self.check(i)
+                if tmp:
+                    ret.append(tmp)
+            return ret
+        else:
+            raise TypeError(type(id))  # pragma: no cover
diff --git a/codepack/service/storage_service/memory_storage_service.py b/codepack/service/storage_service/memory_storage_service.py
new file mode 100644
index 0000000..8d2d1b2
--- /dev/null
+++ b/codepack/service/storage_service/memory_storage_service.py
@@ -0,0 +1,36 @@
+from codepack.storage import MemoryStorage
+from codepack.service.storage_service import StorageService
+from collections.abc import Iterable
+
+
+class MemoryStorageService(StorageService, MemoryStorage):
+    def __init__(self, obj=None):
+        MemoryStorage.__init__(self, obj=obj)
+
+    def save(self, item):
+        if isinstance(item, self.obj):
+            self.memory[item.id] = item
+        else:
+            raise TypeError(type(item))
+
+    def load(self, id):
+        return self.memory[id]
+
+    def remove(self, id):
+        self.memory.pop(id, None)
+
+    def check(self, id):
+        if isinstance(id, str):
+            if id in self.memory:
+                return self.memory[id].id
+            else:
+                return None
+        elif isinstance(id, Iterable):
+            ret = list()
+            for i in id:
+                tmp = self.check(i)
+                if tmp:
+                    ret.append(tmp)
+            return ret
+        else:
+            raise TypeError(type(id))  # pragma: no cover
diff --git a/codepack/service/storage_service/mongo_storage_service.py b/codepack/service/storage_service/mongo_storage_service.py
new file mode 100644
index 0000000..a31aae4
--- /dev/null
+++ b/codepack/service/storage_service/mongo_storage_service.py
@@ -0,0 +1,35 @@
+from codepack.storage import MongoStorage
+from codepack.service.storage_service import StorageService
+from collections.abc import Iterable
+
+
+class MongoStorageService(StorageService, MongoStorage):
+    def __init__(self, obj=None, mongodb=None, db=None, collection=None, *args, **kwargs):
+        MongoStorage.__init__(self, obj=obj, mongodb=mongodb, db=db, collection=collection, *args, **kwargs)
+
+    def save(self, item):
+        if isinstance(item, self.obj):
+            item.to_db(mongodb=self.mongodb, db=self.db, collection=self.collection)
+        else:
+            raise TypeError(type(item))
+
+    def load(self, id):
+        return self.obj.from_db(id=id, mongodb=self.mongodb, db=self.db, collection=self.collection)
+
+    def remove(self, id):
+        self.mongodb[self.db][self.collection].delete_one({'_id': id})
+
+    def check(self, id):
+        if isinstance(id, str):
+            ret = None
+            item = self.obj.from_db(id=id, mongodb=self.mongodb, db=self.db, collection=self.collection)
+            if item:
+                ret = item.id
+            return ret
+        elif isinstance(id, Iterable):
+            ret = list()
+            for item in self.mongodb[self.db][self.collection].find({'_id': {'$in': id}}, projection={}):
+                ret.append(item['_id'])
+            return ret
+        else:
+            raise TypeError(type(id))  # pragma: no cover
diff --git a/codepack/service/storage_service/storage_service.py b/codepack/service/storage_service/storage_service.py
new file mode 100644
index 0000000..fa731f1
--- /dev/null
+++ b/codepack/service/storage_service/storage_service.py
@@ -0,0 +1,20 @@
+import abc
+from codepack.service.service import Service
+
+
+class StorageService(Service, metaclass=abc.ABCMeta):
+    @abc.abstractmethod
+    def save(self, item):
+        """save item"""
+
+    @abc.abstractmethod
+    def load(self, id):
+        """load item with given id"""
+
+    @abc.abstractmethod
+    def remove(self, id):
+        """remove item with given id"""
+
+    @abc.abstractmethod
+    def check(self, id):
+        """check if item with given id exists"""
diff --git a/codepack/snapshot/__init__.py b/codepack/snapshot/__init__.py
new file mode 100644
index 0000000..21e00ea
--- /dev/null
+++ b/codepack/snapshot/__init__.py
@@ -0,0 +1,4 @@
+from codepack.snapshot.snapshot import Snapshot
+from codepack.snapshot.snapshotable import Snapshotable
+from codepack.snapshot.code_snapshot import CodeSnapshot
+from codepack.snapshot.codepack_snapshot import CodePackSnapshot
diff --git a/codepack/snapshot/code_snapshot.py b/codepack/snapshot/code_snapshot.py
new file mode 100644
index 0000000..26718d3
--- /dev/null
+++ b/codepack/snapshot/code_snapshot.py
@@ -0,0 +1,46 @@
+from codepack.snapshot import Snapshot
+from codepack.utils.dependency import Dependency
+
+
+class CodeSnapshot(Snapshot):
+    def __init__(self, code=None, args=None, kwargs=None, timestamp=None):
+        if code:
+            _id = code.id
+            _serial_number = code.serial_number
+            _state = code.get_state()
+            _source = code.source
+            _dependency = code.dependency
+        else:
+            _id = None
+            _serial_number = None
+            _state = None
+            _source = None
+            _dependency = None
+        super().__init__(id=_id, serial_number=_serial_number, state=_state, timestamp=timestamp)
+        self.__setitem__('source', _source)
+        self.set_args(args=args, kwargs=kwargs)
+        self.set_dependency(dependency=_dependency)
+
+    def set_args(self, args=None, kwargs=None):
+        self.__setitem__('args', list(args) if args else list())
+        self.__setitem__('kwargs', kwargs if kwargs else dict())
+
+    def set_dependency(self, dependency):
+        self.__setitem__('dependency', list())
+        if dependency:
+            for d in dependency.values():
+                if isinstance(d, Dependency):
+                    self.__getitem__('dependency').append(d.to_dict())
+                elif isinstance(d, dict):
+                    self.__getitem__('dependency').append(d)
+                else:
+                    raise TypeError(type(d))  # pragma: no cover
+            self.__getitem__('dependency').sort(key=lambda x: x['serial_number'])
+
+    @classmethod
+    def from_dict(cls, d):
+        ret = cls()
+        for k, v in d.items():
+            if k != '_id':
+                ret[k] = v
+        return ret
diff --git a/codepack/snapshot/codepack_snapshot.py b/codepack/snapshot/codepack_snapshot.py
new file mode 100644
index 0000000..bf5325b
--- /dev/null
+++ b/codepack/snapshot/codepack_snapshot.py
@@ -0,0 +1,38 @@
+from codepack.snapshot import Snapshot
+
+
+class CodePackSnapshot(Snapshot):
+    def __init__(self, codepack=None, argpack=None, timestamp=None):
+        if codepack:
+            _id = codepack.id
+            _serial_number = codepack.serial_number
+            _state = codepack.get_state()
+            _codes = {k: v.serial_number for k, v in codepack.codes.items()}
+            _source = codepack.get_source()
+            _structure = codepack.get_structure()
+            _subscribe = codepack.subscribe
+        else:
+            _id = None
+            _serial_number = None
+            _state = None
+            _codes = None
+            _source = None
+            _structure = None
+            _subscribe = None
+        super().__init__(id=_id, serial_number=_serial_number, state=_state, timestamp=timestamp)
+        self.__setitem__('codes', _codes)
+        self.__setitem__('source', _source)
+        self.__setitem__('structure', _structure)
+        self.__setitem__('subscribe', _subscribe)
+        self.set_argpack(argpack=argpack)
+
+    def set_argpack(self, argpack=None):
+        self.__setitem__('argpack', argpack if argpack else dict())
+
+    @classmethod
+    def from_dict(cls, d):
+        ret = cls()
+        for k, v in d.items():
+            if k != '_id':
+                ret[k] = v
+        return ret
diff --git a/codepack/snapshot/snapshot.py b/codepack/snapshot/snapshot.py
new file mode 100644
index 0000000..c782cec
--- /dev/null
+++ b/codepack/snapshot/snapshot.py
@@ -0,0 +1,67 @@
+from codepack.storage import Storable
+from datetime import datetime
+from codepack.utils.state import State
+from copy import deepcopy
+
+
+class Snapshot(Storable):
+    def __init__(self, id, serial_number, state=None, timestamp=None, **kwargs):
+        super().__init__(id=id, serial_number=serial_number)
+        self.attr = dict()
+        self.__setitem__('id', self.id)
+        self.__setitem__('serial_number', self.serial_number)
+        self.__setitem__('state', State.get(state))
+        self.__setitem__('timestamp', timestamp if timestamp else datetime.now().timestamp())
+        for k, v in kwargs.items():
+            self.__setitem__(k, v)
+
+    def __setitem__(self, key, value):
+        if key in ['id', 'serial_number']:
+            self.__setattr__(key, value)
+        if key == 'state':
+            value = State.get(value)
+        self.attr[key] = value
+
+    def __getitem__(self, item):
+        return self.attr[item]
+
+    def __getattr__(self, item):
+        return self.__getitem__(item)
+
+    def diff(self, snapshot):
+        ret = dict()
+        if isinstance(snapshot, self.__class__):
+            for k, v in snapshot.items():
+                if k not in self.attr:
+                    ret[k] = v
+                elif v != self.__getitem__(k):
+                    ret[k] = v
+        elif isinstance(snapshot, dict):
+            return self.diff(self.__class__.from_dict(snapshot))
+        else:
+            raise TypeError(type(snapshot))  # pragma: no cover
+        return ret
+
+    def to_dict(self):
+        ret = deepcopy(self.attr)
+        ret['state'] = ret['state'].name
+        ret['_id'] = ret['serial_number']
+        return ret
+
+    @classmethod
+    def from_dict(cls, d):
+        attr = deepcopy(d)
+        attr.pop('_id', None)
+        return cls(**attr)
+
+    def __iter__(self):
+        return self.attr.__iter__()
+
+    def items(self):
+        return self.attr.items()
+
+    def keys(self):
+        return self.atttr.keys()
+
+    def values(self):
+        return self.attr.values()
diff --git a/codepack/snapshot/snapshotable.py b/codepack/snapshot/snapshotable.py
new file mode 100644
index 0000000..4f02499
--- /dev/null
+++ b/codepack/snapshot/snapshotable.py
@@ -0,0 +1,12 @@
+import abc
+
+
+class Snapshotable(metaclass=abc.ABCMeta):
+    @abc.abstractmethod
+    def to_snapshot(self, *args, **kwargs):
+        """convert to snapshot"""
+
+    @classmethod
+    @abc.abstractmethod
+    def from_snapshot(cls, snapshot):
+        """create instance from snapshot"""
diff --git a/codepack/storage/__init__.py b/codepack/storage/__init__.py
new file mode 100644
index 0000000..7ce299b
--- /dev/null
+++ b/codepack/storage/__init__.py
@@ -0,0 +1,5 @@
+from codepack.storage.storable import Storable
+from codepack.storage.storage import Storage
+from codepack.storage.memory_storage import MemoryStorage
+from codepack.storage.file_storage import FileStorage
+from codepack.storage.mongo_storage import MongoStorage
diff --git a/codepack/storage/file_storage.py b/codepack/storage/file_storage.py
new file mode 100644
index 0000000..2b14976
--- /dev/null
+++ b/codepack/storage/file_storage.py
@@ -0,0 +1,11 @@
+from codepack.storage import Storage
+
+
+class FileStorage(Storage):
+    def __init__(self, obj=None, path='./'):
+        super().__init__(obj=obj)
+        self.path = None
+        self.init(path=path)
+
+    def init(self, path='./'):
+        self.path = path
diff --git a/codepack/storage/memory_storage.py b/codepack/storage/memory_storage.py
new file mode 100644
index 0000000..df2b144
--- /dev/null
+++ b/codepack/storage/memory_storage.py
@@ -0,0 +1,11 @@
+from codepack.storage import Storage
+
+
+class MemoryStorage(Storage):
+    def __init__(self, obj=None):
+        super().__init__(obj=obj)
+        self.memory = None
+        self.init()
+
+    def init(self):
+        self.memory = dict()
diff --git a/codepack/storage/mongo_storage.py b/codepack/storage/mongo_storage.py
new file mode 100644
index 0000000..4e10c32
--- /dev/null
+++ b/codepack/storage/mongo_storage.py
@@ -0,0 +1,24 @@
+from codepack.interface import MongoDB
+from codepack.storage import Storage
+
+
+class MongoStorage(Storage):
+    def __init__(self, obj=None, mongodb=None, db=None, collection=None, *args, **kwargs):
+        super().__init__(obj=obj)
+        self.mongodb = None
+        self.db = None
+        self.collection = None
+        self.new_connection = None
+        self.init(mongodb=mongodb, db=db, collection=collection, *args, **kwargs)
+
+    def init(self, mongodb=None, db=None, collection=None, *args, **kwargs):
+        self.db = db
+        self.collection = collection
+        if isinstance(mongodb, MongoDB):
+            self.mongodb = mongodb
+            self.new_connection = False
+        elif isinstance(mongodb, dict):
+            self.mongodb = MongoDB(mongodb, *args, **kwargs)
+            self.new_connection = True
+        else:
+            raise TypeError(type(mongodb))
diff --git a/codepack/abc.py b/codepack/storage/storable.py
similarity index 57%
rename from codepack/abc.py
rename to codepack/storage/storable.py
index 1fe8edd..65c6a6d 100644
--- a/codepack/abc.py
+++ b/codepack/storage/storable.py
@@ -7,9 +7,8 @@
 
 
 class Storable(metaclass=abc.ABCMeta):
-    @abc.abstractmethod
     def __init__(self, id=None, serial_number=None):
-        """initialize an instance"""
+        """initialize instance"""
         self.id = id
         self.serial_number = serial_number if serial_number else self.generate_serial_number()
 
@@ -20,9 +19,9 @@ def to_json(self):
         return json.dumps(self.to_dict(), default=json_util.default)
 
     @classmethod
-    def from_json(cls, j, *args, **kwargs):
+    def from_json(cls, j):
         d = json.loads(j, object_hook=json_util.object_hook)
-        return cls.from_dict(d, *args, **kwargs)
+        return cls.from_dict(d)
 
     def to_binary(self):
         return bson.Binary(dill.dumps(self))
@@ -36,43 +35,31 @@ def to_file(self, path):
             f.write(self.to_json())
 
     @classmethod
-    def from_file(cls, path, *args, **kwargs):
+    def from_file(cls, path):
         with open(path, 'r') as f:
-            ret = cls.from_json(f.read(), *args, **kwargs)
+            ret = cls.from_json(f.read())
         return ret
 
     def to_db(self, mongodb, db, collection, *args, **kwargs):
-        mongodb[db][collection].insert_one(self.to_dict(*args, **kwargs))
+        mongodb[db][collection].insert_one(self.to_dict(), *args, **kwargs)
 
     @classmethod
     def from_db(cls, id, mongodb, db, collection, *args, **kwargs):
-        d = mongodb[db][collection].find_one({'_id': id})
+        d = mongodb[db][collection].find_one({'_id': id}, *args, **kwargs)
         if d is None:
             return d
         else:
-            return cls.from_dict(d, *args, **kwargs)
+            return cls.from_dict(d)
 
     @abc.abstractmethod
-    def to_dict(self, *args, **kwargs):
-        """save to dict"""
+    def to_dict(self):
+        """convert to dict"""
 
     @classmethod
     @abc.abstractmethod
-    def from_dict(cls, d, *args, **kwargs):
-        """load from dict"""
+    def from_dict(cls, d):
+        """create instance from dict"""
 
     @classmethod
     def get_path(cls, serial_number, path='./'):
         return '%s%s.json' % (path, serial_number)
-
-
-class CodeBase(Storable, metaclass=abc.ABCMeta):
-    @abc.abstractmethod
-    def __init__(self, id=None, serial_number=None):
-        super().__init__(id=id, serial_number=serial_number)
-
-
-class CodePackBase(Storable, metaclass=abc.ABCMeta):
-    @abc.abstractmethod
-    def __init__(self, id=None, serial_number=None):
-        super().__init__(id=id, serial_number=serial_number)
diff --git a/codepack/storage/storage.py b/codepack/storage/storage.py
new file mode 100644
index 0000000..bb0e0c0
--- /dev/null
+++ b/codepack/storage/storage.py
@@ -0,0 +1,13 @@
+import abc
+from codepack.storage import Storable
+
+
+class Storage(metaclass=abc.ABCMeta):
+    def __init__(self, obj=None):
+        if isinstance(obj, Storable):
+            raise TypeError(type(obj))
+        self.obj = obj
+
+    @abc.abstractmethod
+    def init(self, *args, **kwargs):
+        """initialize storage"""
diff --git a/codepack/utils/config.py b/codepack/utils/config.py
index 8661594..a7be140 100644
--- a/codepack/utils/config.py
+++ b/codepack/utils/config.py
@@ -1,5 +1,4 @@
 from configparser import ConfigParser
-from codepack.interface import MongoDB
 import os
 
 
@@ -50,8 +49,7 @@ def get_default_service_config(section, config_path=None):
         ret['collection'] = get_default_value(section=section, key='collection', config=config)
         conn_config = get_default_config(section='conn', config_path=config_path)
         conn_config_path = get_default_value(section='conn', key='path', config=conn_config)
-        mongodb_config = get_config(conn_config_path, 'mongodb')
-        ret['mongodb'] = MongoDB(config=mongodb_config)
+        ret['mongodb'] = get_config(conn_config_path, 'mongodb')
     else:
         raise NotImplementedError("'%s' is not implemented" % ret['source'])
     return ret
diff --git a/codepack/utils/delivery.py b/codepack/utils/delivery.py
new file mode 100644
index 0000000..9e7de5a
--- /dev/null
+++ b/codepack/utils/delivery.py
@@ -0,0 +1,31 @@
+from codepack.storage import Storable
+import json
+from datetime import datetime
+
+
+class Delivery(Storable):
+    def __init__(self, id, serial_number, item=None, timestamp=None):
+        super().__init__(id=id, serial_number=serial_number)
+        self.item = item
+        self.timestamp = timestamp if timestamp else datetime.now().timestamp()
+
+    def __str__(self):
+        return '%s(id: %s, serial_number: %s)' % (self.__class__.__name__, self.id, self.serial_number)  # pragma: no cover
+
+    def __repr__(self):
+        return self.__str__()  # pragma: no cover
+
+    def send(self, item, timestamp=None):
+        timestamp if timestamp else datetime.now().timestamp()
+        self.item = item
+
+    def receive(self):
+        return self.item
+
+    def to_dict(self):
+        return {'_id': self.serial_number, 'id': self.id,
+                'timestamp': self.timestamp, 'item': json.dumps(self.item)}
+
+    @classmethod
+    def from_dict(cls, d):
+        return cls(id=d['id'], serial_number=d['_id'], item=json.loads(d['item']), timestamp=d.get('timestamp', None))
diff --git a/codepack/utils/dependency.py b/codepack/utils/dependency.py
index 23bf8d5..b041b23 100644
--- a/codepack/utils/dependency.py
+++ b/codepack/utils/dependency.py
@@ -1,4 +1,5 @@
-from codepack.abc import CodeBase, Storable
+from codepack.base import CodeBase
+from codepack.storage import Storable
 
 
 class Dependency(Storable):
@@ -47,5 +48,5 @@ def to_dict(self):
         return {'id': self.id, 'serial_number': self.serial_number, 'arg': self.arg}
 
     @classmethod
-    def from_dict(cls, d, *args, **kwargs):
+    def from_dict(cls, d):
         return cls(id=d['id'], serial_number=d['serial_number'], arg=d['arg'])
diff --git a/codepack/utils/dependency_state.py b/codepack/utils/dependency_state.py
new file mode 100644
index 0000000..f3f799f
--- /dev/null
+++ b/codepack/utils/dependency_state.py
@@ -0,0 +1,23 @@
+from enum import Enum
+
+
+class DependencyState(Enum):
+    READY = 0
+    NOT_READY = 1
+    ERROR = 2
+
+    def __str__(self):
+        return self.name  # pragma: no cover
+
+    def __repr__(self):
+        return self.__str__()  # pragma: no cover
+
+    def __eq__(self, other):
+        if isinstance(other, type(self)):
+            return self.name == other.name
+        elif isinstance(other, str):
+            return self.name == other
+        elif isinstance(other, int):
+            return self.name == self.__class__(other).name
+        else:
+            return False  # pragma: no cover
diff --git a/codepack/utils/order.py b/codepack/utils/order.py
deleted file mode 100644
index e9da992..0000000
--- a/codepack/utils/order.py
+++ /dev/null
@@ -1,32 +0,0 @@
-from codepack.abc import Storable
-import json
-from datetime import datetime
-
-
-class Order(Storable):
-    def __init__(self, sender, invoice_number, item=None, send_time=None):
-        super().__init__()
-        self.sender = sender
-        self.invoice_number = invoice_number
-        self.item = item
-        self.send_time = send_time if send_time else datetime.now().timestamp()
-
-    def __str__(self):
-        return '%s(sender: %s, invoice_number: %s)' % (self.__class__.__name__, self.sender, self.invoice_number)  # pragma: no cover
-
-    def __repr__(self):
-        return self.__str__()  # pragma: no cover
-
-    def send(self, item):
-        self.item = item
-
-    def receive(self):
-        return self.item
-
-    def to_dict(self):
-        return {'_id': self.invoice_number, 'sender': self.sender,
-                'send_time': self.send_time, 'item': json.dumps(self.item)}
-
-    @classmethod
-    def from_dict(cls, d, *args, **kwargs):
-        return cls(sender=d['sender'], invoice_number=d['_id'], item=json.loads(d['item']), send_time=d.get('send_time', None))
diff --git a/codepack/utils/state.py b/codepack/utils/state.py
index 5d47f42..5f3640c 100644
--- a/codepack/utils/state.py
+++ b/codepack/utils/state.py
@@ -1,10 +1,7 @@
-from codepack.abc import Storable
 from enum import Enum
-from datetime import datetime
-from codepack.utils.dependency import Dependency
 
 
-class StateCode(Enum):
+class State(Enum):
     UNKNOWN = 0
     NEW = 1
     READY = 2
@@ -24,82 +21,18 @@ def __eq__(self, other):
             return self.name == other.name
         elif isinstance(other, str):
             return self.name == other
-        elif isinstance(other, State):
-            return self == other.state
-        elif isinstance(other, dict):
-            return self == StateCode[other['state']]
         elif isinstance(other, int):
-            return self.name == StateCode(other).name
+            return self.name == State(other).name
         else:
             return False  # pragma: no cover
 
-
-class State(Storable):
-    def __init__(self, id, serial_number, state, update_time=None, args=None, kwargs=None, dependency=None):
-        super().__init__(id=id)
-        self.serial_number = serial_number
-        self.state = self.get_state_code(state)
-        self.update_time = update_time if update_time else datetime.now().timestamp()
-        self.args = None
-        self.kwargs = None
-        self.dependency = dict()
-        self.set_args(args=args, kwargs=kwargs)
-        self.set_dependency(dependency=dependency)
-
-    def to_dict(self):
-        return {'id': self.id, '_id': self.serial_number,
-                'state': self.state.name, 'update_time': self.update_time,
-                'args': self.args, 'kwargs': self.kwargs, 'dependency': self.dependency}
-
     @classmethod
-    def from_dict(cls, d, *args, **kwargs):
-        return cls(id=d['id'], serial_number=d['_id'], state=StateCode[d['state']], update_time=d.get('update_time', None),
-                   args=d.get('args', None), kwargs=d.get('kwargs', None), dependency=d.get('dependency', None))
-
-    def set(self, state, update_time=None, args=None, kwargs=None, dependency=None):
-        if update_time is None:
-            update_time = datetime.now()
-        self.state = self.get_state_code(state)
-        self.update_time = update_time
-        self.set_args(args=args, kwargs=kwargs)
-        self.set_dependency(dependency=dependency)
-
-    def set_args(self, args=None, kwargs=None):
-        self.args = list(args) if args else list()
-        self.kwargs = kwargs if kwargs else dict()
-
-    def set_dependency(self, dependency=None):
-        if dependency:
-            for k, v in dependency.items():
-                if isinstance(v, Dependency):
-                    self.dependency[k] = v.to_dict()
-                elif isinstance(v, dict):
-                    self.dependency[k] = v
-                else:
-                    raise TypeError(type(v))  # pragma: no cover
-
-    def get(self):
-        return self.state
-
-    @staticmethod
-    def get_state_code(state):
-        if isinstance(state, StateCode):
+    def get(cls, state):
+        if isinstance(state, cls):
             return state
         elif isinstance(state, str):
-            return StateCode[state]
+            return cls[state]
         elif isinstance(state, int):
-            return StateCode(state)
+            return cls(state)
         else:
-            raise TypeError(type(state))  # pragma: no cover
-
-    def __eq__(self, other):
-        if isinstance(other, type(self)):
-            return self.state == other.state
-        else:
-            return self.state == other
-
-    def __str__(self):
-        return self.state.__str__()  # pragma: no cover
-
-    def __repr__(self):
-        return self.__str__()  # pragma: no cover
+            return cls.UNKNOWN  # pragma: no cover
diff --git a/config/codepack.ini b/config/codepack.ini
index 6c49ebc..6d3ee71 100644
--- a/config/codepack.ini
+++ b/config/codepack.ini
@@ -6,22 +6,27 @@ source = mongodb
 db = codepack
 collection = scheduler
 
-[code]
+[code_storage]
 source = mongodb
 db = codepack
-collection = codes
+collection = code
 
-[codepack]
+[codepack_storage]
 source = mongodb
 db = codepack
-collection = codepacks
+collection = codepack
 
 [cache]
 source = mongodb
 db = codepack
 collection = cache
 
-[state]
+[code_snapshot]
 source = mongodb
 db = codepack
-collection = state
+collection = code_snapshot
+
+[codepack_snapshot]
+source = mongodb
+db = codepack
+collection = codepack_snapshot


Test patch:
diff --git a/config/test.ini b/config/test.ini
index c60ad89..62743c5 100644
--- a/config/test.ini
+++ b/config/test.ini
@@ -6,14 +6,17 @@ source = mongodb
 db = codepack
 collection = scheduler
 
-[code]
+[code_storage]
 source = memory
 
-[codepack]
+[codepack_storage]
 source = memory
 
 [cache]
 source = memory
 
-[state]
+[code_snapshot]
+source = memory
+
+[codepack_snapshot]
 source = memory
diff --git a/tests/conftest.py b/tests/conftest.py
index 5e51855..3195de3 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -2,7 +2,8 @@
 from codepack.interface import MongoDB
 import os
 import mongomock
-from codepack.service import DefaultServicePack
+from codepack.service import DefaultService
+from codepack import Code, CodePack
 from shutil import rmtree
 from glob import glob
 
@@ -30,6 +31,10 @@ def empty_dir(directory):
 @pytest.fixture(scope='function', autouse=False)
 def default_os_env():
     os.environ['CODEPACK_CONFIG_PATH'] = 'config/test.ini'
+    DefaultService.get_default_delivery_service().init()
+    DefaultService.get_default_code_snapshot_service().init()
+    DefaultService.get_default_code_storage_service(obj=Code).init()
+    DefaultService.get_default_codepack_storage_service(obj=CodePack).init()
     yield
     os.environ.pop('CODEPACK_CONFIG_PATH', None)
 
@@ -37,7 +42,7 @@ def default_os_env():
 @pytest.fixture(scope='function', autouse=False)
 def fake_mongodb():
     mongodb = MongoDB({'host': 'unknown', 'port': '0'})
-    mongodb.client = mongomock.MongoClient()
+    mongodb.session = mongomock.MongoClient()
     yield mongodb
     mongodb.close()
 
@@ -49,6 +54,7 @@ def testdir():
     mkdir(rootdir + 'delivery_service/')
     mkdir(rootdir + 'state_manager/')
     mkdir(rootdir + 'storage_service/')
+    mkdir(rootdir + 'snapshot_service/')
     yield
     rmdir(rootdir)
 
@@ -62,28 +68,28 @@ def testdir_delivery_service():
 
 
 @pytest.fixture(scope='function', autouse=False)
-def testdir_state_manager():
-    directory = 'testdir/state_manager/'
+def testdir_storage_service():
+    directory = 'testdir/storage_service/'
     empty_dir(directory)
     yield directory
     empty_dir(directory)
 
 
 @pytest.fixture(scope='function', autouse=False)
-def testdir_storage_service():
-    directory = 'testdir/storage_service/'
+def testdir_snapshot_service():
+    directory = 'testdir/snapshot_service/'
     empty_dir(directory)
     yield directory
     empty_dir(directory)
 
 
 @pytest.fixture(scope='function', autouse=True)
-def init_default_service_pack():
-    DefaultServicePack.init()
+def init_default_service():
+    DefaultService.init()
 
 
 @pytest.fixture(scope='function', autouse=False)
 def default_services():
-    DefaultServicePack.init()
-    yield DefaultServicePack
-    DefaultServicePack.init()
+    DefaultService.init()
+    yield DefaultService
+    DefaultService.init()
diff --git a/tests/test_code.py b/tests/test_code.py
index 870bbac..404a7f6 100644
--- a/tests/test_code.py
+++ b/tests/test_code.py
@@ -1,6 +1,7 @@
 from codepack import Code
 from tests import *
 import pytest
+from datetime import datetime
 
 
 def test_assert_arg(default_os_env):
@@ -79,7 +80,7 @@ def test_add_dependency(default_os_env):
     assert len(tmp) == 1 and 'a' in tmp
 
 
-def test_check_dependency1(default_os_env):
+def test_check_dependency_linkage(default_os_env):
     code1 = Code(add2)
     code2 = Code(print_x)
     code3 = Code(add3)
@@ -106,7 +107,7 @@ def test_check_dependency1(default_os_env):
     assert len(code2.parents) == 0 and len(code3.parents) == 0
 
 
-def test_check_dependency2(default_os_env):
+def test_check_dependency_state(default_os_env):
     code1 = Code(add2)
     code2 = Code(mul2)
     code3 = Code(add3)
@@ -136,6 +137,57 @@ def test_check_dependency2(default_os_env):
     assert ret == 8 and code3.get_state() == 'TERMINATED'
 
 
+def test_dependency_error_propagation(default_os_env):
+    code1 = Code(add2)
+    code2 = Code(mul2)
+    code3 = Code(add3)
+    code1 >> code3
+    code2 >> code3
+    assert code3.get_state() == 'UNKNOWN'
+    with pytest.raises(TypeError):
+        code1()
+    assert code1.get_state() == 'ERROR'
+    ret = code3(1, 2, 3)
+    assert ret is None
+    assert code3.get_state() == 'ERROR'
+
+
+def test_validate_dependency_result(default_os_env):
+    code1 = Code(add2)
+    code2 = Code(mul2)
+    code3 = Code(add3)
+    code1 >> code3
+    code2 >> code3
+    code3.receive('b') << code1
+    code3.receive('c') << code2
+    code1(1, 2)
+    code2(3, 4)
+    code2.service['delivery_service'].cancel(code2.serial_number)
+    ret = code3(a=3)
+    assert ret is None
+    assert code3.get_state() == 'WAITING'
+    code2(3, 4)
+    code2.send_result(item=1, timestamp=datetime.now().timestamp() + 1)
+    ret = code3(a=3)
+    assert ret is None
+    assert code3.get_state() == 'WAITING'
+    code2(3, 4)
+    snapshots = code3.get_dependent_snapshots()
+    caches = code3.get_dependency_cache_info()
+    assert len(snapshots) == 2
+    assert len(caches) == 2
+    code2_state_info = snapshots.pop(code2.serial_number)
+    assert code3.validate_dependency_result(snapshots=snapshots, caches=caches) == 'NOT_READY'
+    update_time = code2_state_info.pop('timestamp')
+    snapshots[code2.serial_number] = code2_state_info
+    assert code3.validate_dependency_result(snapshots=snapshots, caches=caches) == 'NOT_READY'
+    snapshots[code2.serial_number]['timestamp'] = update_time
+    send_time = caches[code2.serial_number].pop('timestamp')
+    assert code3.validate_dependency_result(snapshots=snapshots, caches=caches) == 'NOT_READY'
+    caches[code2.serial_number]['timestamp'] = send_time
+    assert code3.validate_dependency_result(snapshots=snapshots, caches=caches) == 'READY'
+
+
 def test_default_arg(default_os_env):
     code1 = Code(add2)
     code2 = Code(add3)
@@ -162,3 +214,37 @@ def test_load_code_from_storage_service_with_id(default_os_env):
     assert code1(1, 2) == code2(1, 2)
     assert code1.serial_number != code2.serial_number
     assert code2.serial_number == '1234'
+
+
+def test_update_serial_number(default_os_env):
+    code1 = Code(add2)
+    code2 = Code(mul2)
+    code3 = Code(add3)
+    code4 = Code(linear)
+    code1 >> code2
+    code2 >> [code3, code4]
+    code2.receive('a') << code1
+    code3.receive('b') << code2
+    code4.receive('c') << code2
+    old_serial_number = code2.serial_number
+    new_serial_number = '1234'
+    code2.update_serial_number(new_serial_number)
+    assert code2.serial_number == new_serial_number
+    assert code2.id in code1.children
+    assert code1.children[code2.id].serial_number == new_serial_number
+    assert code2.id in code3.parents
+    assert code3.parents[code2.id].serial_number == new_serial_number
+    assert old_serial_number not in code3.dependency
+    assert new_serial_number in code3.dependency
+    assert code3.dependency[new_serial_number].arg == 'b'
+    assert code3.dependency[new_serial_number].code == code3
+    assert code3.dependency[new_serial_number].id == code2.id
+    assert code3.dependency[new_serial_number].serial_number == new_serial_number
+    assert code2.id in code4.parents
+    assert code4.parents[code2.id].serial_number == new_serial_number
+    assert old_serial_number not in code4.dependency
+    assert new_serial_number in code4.dependency
+    assert code4.dependency[new_serial_number].arg == 'c'
+    assert code4.dependency[new_serial_number].code == code4
+    assert code4.dependency[new_serial_number].id == code2.id
+    assert code4.dependency[new_serial_number].serial_number == new_serial_number
diff --git a/tests/test_codepack.py b/tests/test_codepack.py
index 6cc2167..7ae6d00 100644
--- a/tests/test_codepack.py
+++ b/tests/test_codepack.py
@@ -2,7 +2,7 @@
 from tests import *
 
 
-def test_codepack(default_os_env):
+def test_sync_codepack(default_os_env):
     c1 = Code(add3)
     c2 = Code(mul2)
     c3 = Code(combination)
@@ -19,15 +19,15 @@ def test_codepack(default_os_env):
     c5.receive('x') << c3
 
     cp = CodePack(id='test_codepack', code=c1, subscribe=c4)
-    arg_dict = cp.make_arg_dict()
-    arg_dict['add3']['a'] = 1
-    arg_dict['add3']['b'] = 2
-    arg_dict['add3']['c'] = 3
-    arg_dict['mul2']['a'] = 1
-    arg_dict['mul2']['b'] = 2
-    arg_dict['combination']['a'] = 2
-    arg_dict['combination']['b'] = 5
-    arg_dict['linear']['b'] = 7
-    arg_dict['linear']['a'] = 5
-    ret = cp(arg_dict)
+    argpack = cp.make_argpack()
+    argpack['add3']['a'] = 1
+    argpack['add3']['b'] = 2
+    argpack['add3']['c'] = 3
+    argpack['mul2']['a'] = 1
+    argpack['mul2']['b'] = 2
+    argpack['combination']['a'] = 2
+    argpack['combination']['b'] = 5
+    argpack['linear']['b'] = 7
+    argpack['linear']['a'] = 5
+    ret = cp(argpack)
     assert ret == 57
diff --git a/tests/test_delivery_service.py b/tests/test_delivery_service.py
index da4daba..1592d03 100644
--- a/tests/test_delivery_service.py
+++ b/tests/test_delivery_service.py
@@ -1,59 +1,46 @@
 from codepack.service import MemoryDeliveryService, FileDeliveryService, MongoDeliveryService
-from codepack.utils.order import Order
+from codepack.utils.delivery import Delivery
 import os
 
 
-def test_singleton_memory_delivery_service():
-    mds1 = MemoryDeliveryService()
-    mds1.init()
-    sender = 'sender'
-    invoice_number = '1234'
-    item = 'Hello, World!'
-    mds1.send(sender=sender, invoice_number=invoice_number, item=item)
-    mds2 = MemoryDeliveryService()
-    assert mds1 == mds2
-    assert len(mds2.deliveries) == 1
-    assert mds2.check(invoice_number=invoice_number)
-
-
 def test_memory_delivery_service_check():
-    mds = MemoryDeliveryService()
+    mds = MemoryDeliveryService(obj=Delivery)
     mds.init()
     sender = 'sender'
-    invoice_number = '1234'
+    serial_number = '1234'
     item = 'Hello, World!'
-    mds.send(sender=sender, invoice_number=invoice_number, item=item)
-    assert isinstance(mds.check(invoice_number=invoice_number), dict)
-    mds.cancel(invoice_number=invoice_number)
-    invoice_numbers = ['123', '456', '789']
-    mds.send(sender=sender, invoice_number=invoice_numbers[0], item=item)
-    mds.send(sender=sender, invoice_number=invoice_numbers[1], item=item)
-    check = mds.check(invoice_number=invoice_numbers)
+    mds.send(id=sender, serial_number=serial_number, item=item)
+    assert isinstance(mds.check(serial_number=serial_number), dict)
+    mds.cancel(serial_number=serial_number)
+    serial_numbers = ['123', '456', '789']
+    mds.send(id=sender, serial_number=serial_numbers[0], item=item)
+    mds.send(id=sender, serial_number=serial_numbers[1], item=item)
+    check = mds.check(serial_number=serial_numbers)
     assert isinstance(check, list)
     assert len(check) == 2
 
 
 def test_file_delivery_service_check(testdir_delivery_service):
-    fds = FileDeliveryService(path=testdir_delivery_service)
+    fds = FileDeliveryService(obj=Delivery, path=testdir_delivery_service)
     sender = 'sender'
-    invoice_number = '1234'
-    invoice_numbers = ['123', '456', '789']
+    serial_number = '1234'
+    serial_numbers = ['123', '456', '789']
     try:
-        fds.cancel(invoice_number=invoice_number)
+        fds.cancel(serial_number=serial_number)
     except Exception:
         pass
-    for i in invoice_numbers:
+    for i in serial_numbers:
         try:
-            fds.cancel(invoice_number=i)
+            fds.cancel(serial_number=i)
         except Exception:
             continue
     item = 'Hello, World!'
-    fds.send(sender=sender, invoice_number=invoice_number, item=item)
-    assert isinstance(fds.check(invoice_number=invoice_number), dict)
-    fds.cancel(invoice_number=invoice_number)
-    fds.send(sender=sender, invoice_number=invoice_numbers[0], item=item)
-    fds.send(sender=sender, invoice_number=invoice_numbers[1], item=item)
-    check = fds.check(invoice_number=invoice_numbers)
+    fds.send(id=sender, serial_number=serial_number, item=item)
+    assert isinstance(fds.check(serial_number=serial_number), dict)
+    fds.cancel(serial_number=serial_number)
+    fds.send(id=sender, serial_number=serial_numbers[0], item=item)
+    fds.send(id=sender, serial_number=serial_numbers[1], item=item)
+    check = fds.check(serial_number=serial_numbers)
     assert isinstance(check, list)
     assert len(check) == 2
 
@@ -61,70 +48,70 @@ def test_file_delivery_service_check(testdir_delivery_service):
 def test_mongo_delivery_service_check(fake_mongodb):
     db = 'test'
     collection = 'cache'
-    mds = MongoDeliveryService(mongodb=fake_mongodb, db=db, collection=collection)
+    mds = MongoDeliveryService(obj=Delivery, mongodb=fake_mongodb, db=db, collection=collection)
     sender = 'sender'
-    invoice_number = '1234'
-    invoice_numbers = ['123', '456', '789']
-    mds.cancel(invoice_number=invoice_number)
-    for i in invoice_numbers:
-        mds.cancel(invoice_number=i)
+    serial_number = '1234'
+    serial_numbers = ['123', '456', '789']
+    mds.cancel(serial_number=serial_number)
+    for i in serial_numbers:
+        mds.cancel(serial_number=i)
     item = 'Hello, World!'
-    mds.send(sender=sender, invoice_number=invoice_number, item=item)
-    assert isinstance(mds.check(invoice_number=invoice_number), dict)
-    mds.cancel(invoice_number=invoice_number)
-    mds.send(sender=sender, invoice_number=invoice_numbers[0], item=item)
-    mds.send(sender=sender, invoice_number=invoice_numbers[1], item=item)
-    check = mds.check(invoice_number=invoice_numbers)
+    mds.send(id=sender, serial_number=serial_number, item=item)
+    assert isinstance(mds.check(serial_number=serial_number), dict)
+    mds.cancel(serial_number=serial_number)
+    mds.send(id=sender, serial_number=serial_numbers[0], item=item)
+    mds.send(id=sender, serial_number=serial_numbers[1], item=item)
+    check = mds.check(serial_number=serial_numbers)
     assert isinstance(check, list)
     assert len(check) == 2
 
 
 def test_memory_delivery_service():
-    mds = MemoryDeliveryService()
+    mds = MemoryDeliveryService(obj=Delivery)
     mds.init()
     sender = 'sender'
-    invoice_number = '1234'
+    serial_number = '1234'
     item = 'Hello, World!'
-    mds.send(sender=sender, invoice_number=invoice_number, item=item)
-    assert invoice_number in mds.deliveries, "'send' failed"
-    check = mds.check(invoice_number=invoice_number)
-    receive = mds.receive(invoice_number=invoice_number)
-    assert check['sender'] == sender and check['_id'] == invoice_number, "'check' failed"
+    mds.send(id=sender, serial_number=serial_number, item=item)
+    assert serial_number in mds.memory, "'send' failed"
+    check = mds.check(serial_number=serial_number)
+    receive = mds.receive(serial_number=serial_number)
+    assert check['id'] == sender and check['_id'] == serial_number, "'check' failed"
     assert receive == item, "'receive' failed"
-    mds.cancel(invoice_number=invoice_number)
-    check2 = mds.check(invoice_number=invoice_number)
+    mds.cancel(serial_number=serial_number)
+    check2 = mds.check(serial_number=serial_number)
     assert not check2, "'cancel' failed"
 
 
 def test_file_delivery_service(testdir_delivery_service):
-    fds = FileDeliveryService(path=testdir_delivery_service)
+    fds = FileDeliveryService(obj=Delivery, path=testdir_delivery_service)
     sender = 'sender'
-    invoice_number = '1234'
+    serial_number = '1234'
     item = 'Hello, World!'
-    fds.send(sender=sender, invoice_number=invoice_number, item=item)
-    assert os.path.isfile(Order.get_path(serial_number=invoice_number, path=fds.path)), "'send' failed"
-    check = fds.check(invoice_number=invoice_number)
-    receive = fds.receive(invoice_number=invoice_number)
-    assert check['sender'] == sender and check['_id'] == invoice_number, "'check' failed"
+    fds.send(id=sender, serial_number=serial_number, item=item)
+    assert os.path.isfile(Delivery.get_path(serial_number=serial_number, path=fds.path)), "'send' failed"
+    check = fds.check(serial_number=serial_number)
+    receive = fds.receive(serial_number=serial_number)
+    assert check['id'] == sender and check['_id'] == serial_number, "'check' failed"
     assert receive == item, "'receive' error"
-    fds.cancel(invoice_number=invoice_number)
-    check2 = fds.check(invoice_number=invoice_number)
+    fds.cancel(serial_number=serial_number)
+    check2 = fds.check(serial_number=serial_number)
     assert not check2, "'cancel' error"
 
 
 def test_mongo_delivery_service(fake_mongodb):
     db = 'test'
     collection = 'cache'
-    mds = MongoDeliveryService(mongodb=fake_mongodb, db=db, collection=collection)
+    mds = MongoDeliveryService(obj=Delivery, mongodb=fake_mongodb, db=db, collection=collection)
     sender = 'sender'
-    invoice_number = '1234'
+    serial_number = '1234'
     item = 'Hello, World!'
-    mds.send(sender=sender, invoice_number=invoice_number, item=item)
-    assert fake_mongodb[db][collection].find_one({'_id': invoice_number}), "'send' failed"
-    check = mds.check(invoice_number=invoice_number)
-    receive = mds.receive(invoice_number=invoice_number)
-    assert check['sender'] == sender and check['_id'] == invoice_number, "'check' failed"
+    mds.send(id=sender, serial_number=serial_number, item=item)
+    assert fake_mongodb[db][collection].find_one({'_id': serial_number}), "'send' failed"
+    check = mds.check(serial_number=serial_number)
+    receive = mds.receive(serial_number=serial_number)
+    assert check['id'] == sender and check['_id'] == serial_number, "'check' failed"
     assert receive == item, "'receive' error"
-    mds.cancel(invoice_number=invoice_number)
-    check2 = mds.check(invoice_number=invoice_number)
+    mds.cancel(serial_number=serial_number)
+    check2 = mds.check(serial_number=serial_number)
     assert not check2, "'cancel' error"
diff --git a/tests/test_dependency_state.py b/tests/test_dependency_state.py
new file mode 100644
index 0000000..cbf940d
--- /dev/null
+++ b/tests/test_dependency_state.py
@@ -0,0 +1,7 @@
+from codepack.utils.dependency_state import DependencyState
+
+
+def test_eq(default_os_env):
+    assert DependencyState.READY == 'READY'
+    assert DependencyState.READY != DependencyState.NOT_READY
+    assert DependencyState.READY == 0
diff --git a/tests/test_engine.py b/tests/test_engine.py
new file mode 100644
index 0000000..3acd389
--- /dev/null
+++ b/tests/test_engine.py
@@ -0,0 +1,93 @@
+from codepack import Code
+from tests import *
+from codepack.engine import BlockingEngine, NonBlockingEngine
+import time
+
+
+def load_and_run_code_from_snapshot(snapshot):
+    code = Code.from_snapshot(snapshot)
+    code(*snapshot['args'], **snapshot['kwargs'])
+
+
+def load_and_run_code_from_snapshot_and_raise_exception(snapshot):
+    code = Code.from_snapshot(snapshot)
+    code(*snapshot['args'], **snapshot['kwargs'])
+    raise KeyboardInterrupt
+
+
+def test_blocking_engine(default_os_env):
+    be = BlockingEngine(callback=load_and_run_code_from_snapshot_and_raise_exception, interval=0.5)
+    c1 = Code(add3)
+    c2 = Code(mul2)
+    c3 = Code(combination)
+    c4 = Code(linear)
+    c1 >> c3
+    c2 >> c3
+    c3 >> c4
+    c3.receive('c') << c1
+    c3.receive('d') << c2
+    c4.receive('c') << c3
+    c1.save()
+    c2.save()
+    c3.save()
+    c4.save()
+    c3(2, b=3)
+    c4(3, 6)
+    c2(3, 5)
+    c1(1, 2, 3)
+    assert c1.get_state() == 'TERMINATED'
+    assert c2.get_state() == 'TERMINATED'
+    assert c3.get_state() == 'WAITING'
+    assert c4.get_state() == 'WAITING'
+    be.start()
+    assert c1.get_state() == 'TERMINATED'
+    assert c2.get_state() == 'TERMINATED'
+    assert c3.get_state() == 'TERMINATED'
+    assert c4.get_state() == 'WAITING'
+    be.start()
+    assert c1.get_state() == 'TERMINATED'
+    assert c2.get_state() == 'TERMINATED'
+    assert c3.get_state() == 'TERMINATED'
+    assert c4.get_state() == 'TERMINATED'
+    assert c4.get_result() == 114
+    be.stop()
+
+
+def test_non_blocking_engine(default_os_env):
+    nbe = NonBlockingEngine(callback=load_and_run_code_from_snapshot, interval=0.5)
+    c1 = Code(add3)
+    c2 = Code(mul2)
+    c3 = Code(combination)
+    c4 = Code(linear)
+    c5 = Code(print_x)
+    c1 >> c3
+    c2 >> c3
+    c3 >> [c4, c5]
+    c3.receive('c') << c1
+    c3.receive('d') << c2
+    c4.receive('c') << c3
+    c5.receive('x') << c3
+    c1.save()
+    c2.save()
+    c3.save()
+    c4.save()
+    c5.save()
+    c3(2, b=3)
+    c5()
+    c4(3, 6)
+    c2(3, 5)
+    c1(1, 2, 3)
+    assert c1.get_state() == 'TERMINATED'
+    assert c2.get_state() == 'TERMINATED'
+    assert c3.get_state() == 'WAITING'
+    assert c4.get_state() == 'WAITING'
+    assert c5.get_state() == 'WAITING'
+    nbe.start()
+    time.sleep(1)
+    assert c1.get_state() == 'TERMINATED'
+    assert c2.get_state() == 'TERMINATED'
+    assert c3.get_state() == 'TERMINATED'
+    assert c4.get_state() == 'TERMINATED'
+    assert c5.get_state() == 'TERMINATED'
+    assert c4.get_result() == 114
+    nbe.stop()
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
new file mode 100644
index 0000000..a2de90d
--- /dev/null
+++ b/tests/test_scheduler.py
@@ -0,0 +1,28 @@
+from codepack import Code, CodePack
+from codepack.scheduler import MongoScheduler
+from tests import *
+
+
+def test_mongo_jobstore_codepack_snapshot(default_os_env, fake_mongodb):
+    code = Code(hello)
+    codepack = CodePack('codepack_test', code=code, subscribe=code)
+    argpack = codepack.make_argpack()
+    argpack['hello']['name'] = 'CodePack'
+    job_id = 'job_test'
+    db = 'test'
+    collection = 'scheduler'
+    scheduler = MongoScheduler(db=db, collection=collection, mongodb=fake_mongodb, blocking=False)
+    scheduler.add_codepack(job_id=job_id, codepack=codepack, argpack=argpack, trigger='interval', seconds=30)
+    scheduler.start()
+    assert fake_mongodb[db][collection].count_documents({'_id': codepack.id}) == 0
+    assert fake_mongodb[db][collection].count_documents({'_id': job_id}) == 1
+    document = fake_mongodb[db][collection].find_one({'_id': job_id})
+    for k in ['_id', 'trigger', 'codepack', 'snapshot', 'last_run_time', 'next_run_time']:
+        assert k in document
+    assert document['_id'] == job_id
+    assert document['trigger'] == 'interval[0:00:30]'
+    assert document['codepack'] == codepack.id
+    assert document['snapshot'] == codepack.serial_number
+    scheduler.remove_job(job_id)
+    assert fake_mongodb[db][collection].count_documents({'_id': job_id}) == 0
+    scheduler.stop()
diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py
new file mode 100644
index 0000000..809c0d8
--- /dev/null
+++ b/tests/test_snapshot.py
@@ -0,0 +1,154 @@
+from codepack.snapshot import Snapshot
+from codepack.snapshot import CodeSnapshot
+from codepack import Code, CodePack
+from tests import *
+from datetime import datetime
+import pytest
+
+
+def test_snapshot_to_dict_and_from_dict():
+    snapshot1 = Snapshot(id='1234', serial_number='5678', custom_value=9)
+    snapshot_dict1 = snapshot1.to_dict()
+    snapshot2 = Snapshot.from_dict(snapshot_dict1)
+    assert snapshot1.id == snapshot2.id
+    assert snapshot1.serial_number == snapshot2.serial_number
+    assert snapshot1.timestamp == snapshot2.timestamp
+    assert snapshot1.custom_value == snapshot2.custom_value
+    assert snapshot2.custom_value == 9
+    assert '_id' in snapshot_dict1
+    assert '_id' not in snapshot2.attr
+
+
+def test_snapshot_diff():
+    timestamp = datetime.now().timestamp()
+    snapshot1 = Snapshot(id='1234', serial_number='5678', custom_value=9, timestamp=timestamp)
+    snapshot2 = Snapshot(id='1234', serial_number='8765', custom_value=3, timestamp=timestamp + 1)
+    diff = snapshot1.diff(snapshot2)
+    assert set(diff.keys()) == {'serial_number', 'custom_value', 'timestamp'}
+    assert diff['serial_number'] == '8765'
+    assert diff['custom_value'] == 3
+    assert diff['timestamp'] == timestamp + 1
+
+
+def test_code_snapshot_to_dict_and_from_dict(default_os_env):
+    code1 = Code(mul2)
+    code2 = Code(add2)
+    code3 = Code(add3)
+    code1 >> code3
+    code2 >> code3
+    code3.receive('c') << code2
+    code3(1, b=2)
+    snapshot1 = CodeSnapshot(code3, args=(1, ), kwargs={'b': 2})
+    snapshot_dict1 = snapshot1.to_dict()
+    assert 'state' in snapshot_dict1
+    assert snapshot_dict1['state'] == 'WAITING'
+    assert 'source' in snapshot_dict1
+    assert snapshot_dict1['source'] == code3.source
+    assert 'dependency' in snapshot_dict1
+    for dependency in snapshot_dict1['dependency']:
+        code3_dependency = code3.dependency[dependency['serial_number']]
+        assert set(dependency.keys()) == {'id', 'serial_number', 'arg'}
+        assert dependency['id'] == code3_dependency.id
+        assert dependency['serial_number'] == code3_dependency.serial_number
+        assert dependency['arg'] == code3_dependency.arg
+    snapshot2 = CodeSnapshot.from_dict(snapshot_dict1)
+    assert snapshot1.id == snapshot2.id
+    assert snapshot1.serial_number == snapshot2.serial_number
+    assert snapshot1.timestamp == snapshot2.timestamp
+    assert snapshot1.args == snapshot2.args
+    assert snapshot1.kwargs == snapshot2.kwargs
+    assert snapshot1.source == snapshot2.source
+    assert snapshot1.dependency == snapshot2.dependency
+
+
+def test_code_snapshot_diff(default_os_env):
+    timestamp = datetime.now().timestamp()
+    code1 = Code(mul2)
+    code2 = Code(add2)
+    code3 = Code(add3)
+    code1 >> code3
+    code2 >> code3
+    code3.receive('c') << code2
+    code3(1, b=2)
+    snapshot1 = CodeSnapshot(code3, args=(1, ), kwargs={'b': 2}, timestamp=timestamp)
+    snapshot2 = CodeSnapshot.from_dict(snapshot1.to_dict())
+    snapshot3 = CodeSnapshot(code3, args=(1, ), kwargs={'b': 3}, timestamp=timestamp + 1)
+    assert snapshot1.diff(snapshot2) == dict()
+    assert snapshot2.diff(snapshot1) == dict()
+    diff = snapshot1.diff(snapshot3)
+    assert set(diff.keys()) == {'kwargs', 'timestamp'}
+    assert diff['kwargs'] == {'b': 3}
+    assert diff['timestamp'] == timestamp + 1
+
+
+def test_code_to_snapshot_and_from_snapshot(default_os_env):
+    code1 = Code(add2)
+    snapshot1 = code1.to_snapshot(args=(1, 2))
+    code2 = Code.from_snapshot(snapshot=snapshot1)
+    assert code2.get_state() == 'UNKNOWN'
+    assert code2(1, 2) == 3
+
+
+def test_codepack_to_snapshot_and_from_snapshot(default_os_env):
+    c1 = Code(add3)
+    c2 = Code(mul2)
+    c3 = Code(combination)
+    c4 = Code(linear)
+    c5 = Code(print_x)
+
+    c1 >> c3
+    c2 >> c3
+    c3 >> [c4, c5]
+
+    c3.receive('c') << c1
+    c4.receive('c') << c3
+    c5.receive('x') << c3
+
+    cp1 = CodePack(id='test_codepack', code=c1, subscribe=c4)
+
+    argpack = cp1.make_argpack()
+    argpack['add3']['a'] = 1
+    argpack['add3']['b'] = 2
+    argpack['add3']['c'] = 3
+    argpack['mul2']['a'] = 2
+    argpack['mul2']['b'] = 1
+    argpack['combination']['a'] = 2
+    argpack['combination']['b'] = 5
+    argpack['linear']['b'] = 7
+    argpack['linear']['a'] = 5
+
+    ret = None
+    with pytest.raises(TypeError):
+        ret = cp1(argpack)
+
+    assert ret is None
+    assert cp1.get_state() == 'ERROR'
+    snapshot1 = cp1.to_snapshot(argpack=argpack)
+    cp2 = CodePack.from_snapshot(snapshot1)
+    assert cp1.id == cp2.id
+    assert cp1.serial_number == cp2.serial_number
+    assert cp1.get_state() == cp2.get_state()
+    cp1_source = cp1.get_source()
+    cp2_source = cp2.get_source()
+    assert cp1_source.keys() == cp2_source.keys()
+    for id in cp1_source.keys():
+        assert cp1_source[id].strip() == cp2_source[id].strip()
+    # assert cp1.get_structure() == cp2.get_structure()
+    assert cp1.subscribe == cp2.subscribe
+    assert set(cp1.codes.keys()) == set(cp2.codes.keys())
+    for code_id in cp1.codes.keys():
+        code1 = cp1.codes[code_id]
+        code2 = cp2.codes[code_id]
+        assert code1.serial_number == code2.serial_number
+        assert code1.parents.keys() == code2.parents.keys()
+        for id in code1.parents.keys():
+            assert code1.parents[id].serial_number == code2.parents[id].serial_number
+        assert code1.children.keys() == code2.children.keys()
+        for id in code1.children.keys():
+            assert code1.children[id].serial_number == code2.children[id].serial_number
+        assert code1.get_state() == code2.get_state()
+        assert code1.dependency.keys() == code2.dependency.keys()
+        for serial_number in code1.dependency.keys():
+            assert code1.dependency[serial_number].id == code2.dependency[serial_number].id
+            assert code1.dependency[serial_number].serial_number == code2.dependency[serial_number].serial_number
+            assert code1.dependency[serial_number].arg == code2.dependency[serial_number].arg
diff --git a/tests/test_snapshot_service.py b/tests/test_snapshot_service.py
new file mode 100644
index 0000000..bf471d2
--- /dev/null
+++ b/tests/test_snapshot_service.py
@@ -0,0 +1,199 @@
+from codepack.service import MemorySnapshotService, FileSnapshotService, MongoSnapshotService
+from codepack.snapshot import Snapshot
+from codepack.snapshot import CodeSnapshot
+from codepack import Code
+from datetime import datetime
+from tests import *
+import os
+
+
+def test_memory_code_snapshot_service_save_and_load(default_os_env):
+    mss = MemorySnapshotService(obj=CodeSnapshot)
+    mss.init()
+    code = Code(add2)
+    snapshot = CodeSnapshot(code)
+    mss.save(snapshot=snapshot)
+    assert len(mss.memory) == 1
+    assert mss.load(snapshot.serial_number) == snapshot.to_dict()
+    loaded = mss.load(snapshot.serial_number, projection={'state'})
+    assert set(loaded.keys()) == {'serial_number', 'state'}
+    assert 'state' in loaded
+    assert loaded['state'] == 'UNKNOWN'
+    assert 'serial_number' in loaded
+    assert loaded['serial_number'] == snapshot.serial_number
+
+
+def test_memory_code_snapshot_service_search_and_remove(default_os_env):
+    mss = MemorySnapshotService(obj=CodeSnapshot)
+    mss.init()
+    code1 = Code(add2)
+    code2 = Code(add3)
+    code1 >> code2
+    code2(1, 2, 3)
+    snapshot1 = CodeSnapshot(code1)
+    snapshot2 = CodeSnapshot(code2)
+    mss.save(snapshot=snapshot1)
+    mss.save(snapshot=snapshot2)
+    assert len(mss.memory) == 2
+    loaded = mss.load([snapshot1.serial_number, snapshot2.serial_number, '1234'])
+    assert len(loaded) == 2
+    assert loaded[0]['state'] == 'UNKNOWN'
+    assert loaded[1]['state'] == 'WAITING'
+    loaded = mss.load([snapshot1.serial_number, snapshot2.serial_number, '1234'], projection={'state'})
+    for x in loaded:
+        assert set(x.keys()) == {'state', 'serial_number'}
+    search_result = mss.search(key='state', value='WAITING')
+    assert len(search_result) == 1
+    assert search_result[0] == snapshot2.to_dict()
+    search_result = mss.search(key='state', value='WAITING', projection={'state'})
+    assert len(search_result) == 1
+    assert len(search_result[0].keys()) == 2
+    assert search_result[0]['state'] == 'WAITING'
+    assert search_result[0]['serial_number'] == code2.serial_number
+    mss.remove(snapshot2.serial_number)
+    loaded = mss.load([snapshot1.serial_number, snapshot2.serial_number, '1234'])
+    assert len(loaded) == 1
+    search_result = mss.search(key='state', value='WAITING', projection={'state'})
+    assert len(search_result) == 0
+
+
+def test_memory_snapshot_service_update():
+    mss = MemorySnapshotService(obj=Snapshot)
+    mss.init()
+    timestamp = datetime.now().timestamp()
+    snapshot1 = Snapshot(id='1234', serial_number='5678', timestamp=timestamp)
+    snapshot2 = Snapshot(id='1234', serial_number='5678', timestamp=timestamp + 1)
+    mss.save(snapshot=snapshot1)
+    tmp_id = id(mss.memory[snapshot1.serial_number])
+    mss.save(snapshot=snapshot2)
+    assert len(mss.memory) == 1
+    assert tmp_id == id(mss.memory[snapshot2.serial_number])
+    assert mss.memory[snapshot1.serial_number].timestamp == timestamp + 1
+
+
+def test_file_code_snapshot_service_save_and_load(default_os_env, testdir_snapshot_service):
+    fss = FileSnapshotService(obj=CodeSnapshot, path=testdir_snapshot_service)
+    code = Code(add2)
+    snapshot = CodeSnapshot(code)
+    fss.save(snapshot=snapshot)
+    assert len(os.listdir(testdir_snapshot_service)) == 1
+    assert fss.load(snapshot.serial_number) == snapshot.to_dict()
+    loaded = fss.load(snapshot.serial_number, projection={'state'})
+    assert set(loaded.keys()) == {'serial_number', 'state'}
+    assert 'state' in loaded
+    assert loaded['state'] == 'UNKNOWN'
+    assert 'serial_number' in loaded
+    assert loaded['serial_number'] == snapshot.serial_number
+
+
+def test_file_code_snapshot_service_search_and_remove(default_os_env, testdir_snapshot_service):
+    fss = FileSnapshotService(obj=CodeSnapshot, path=testdir_snapshot_service)
+    code1 = Code(add2)
+    code2 = Code(add3)
+    code1 >> code2
+    code2(1, 2, 3)
+    snapshot1 = CodeSnapshot(code1)
+    snapshot2 = CodeSnapshot(code2)
+    fss.save(snapshot=snapshot1)
+    fss.save(snapshot=snapshot2)
+    assert len(os.listdir(testdir_snapshot_service)) == 2
+    loaded = fss.load([snapshot1.serial_number, snapshot2.serial_number, '1234'])
+    assert len(loaded) == 2
+    assert loaded[0]['state'] == 'UNKNOWN'
+    assert loaded[1]['state'] == 'WAITING'
+    loaded = fss.load([snapshot1.serial_number, snapshot2.serial_number, '1234'], projection={'state'})
+    for x in loaded:
+        assert set(x.keys()) == {'state', 'serial_number'}
+    search_result = fss.search(key='state', value='WAITING')
+    assert len(search_result) == 1
+    assert search_result[0] == snapshot2.to_dict()
+    search_result = fss.search(key='state', value='WAITING', projection={'state'})
+    assert len(search_result) == 1
+    assert len(search_result[0].keys()) == 2
+    assert search_result[0]['state'] == 'WAITING'
+    assert search_result[0]['serial_number'] == code2.serial_number
+    fss.remove(snapshot2.serial_number)
+    loaded = fss.load([snapshot1.serial_number, snapshot2.serial_number, '1234'])
+    assert len(loaded) == 1
+    search_result = fss.search(key='state', value='WAITING', projection={'state'})
+    assert len(search_result) == 0
+
+
+def test_file_snapshot_service_update(testdir_snapshot_service):
+    fss = FileSnapshotService(obj=Snapshot, path=testdir_snapshot_service)
+    timestamp = datetime.now().timestamp()
+    snapshot1 = Snapshot(id='1234', serial_number='5678', timestamp=timestamp)
+    snapshot2 = Snapshot(id='1234', serial_number='5678', timestamp=timestamp + 1)
+    fss.save(snapshot=snapshot1)
+    fss.save(snapshot=snapshot2)
+    assert len(os.listdir(testdir_snapshot_service)) == 1
+    search_result = fss.search(key='id', value='1234', projection={'timestamp'})
+    assert len(search_result) == 1
+    assert search_result[0]['timestamp'] == timestamp + 1
+
+
+def test_mongo_code_snapshot_service_save_and_load(default_os_env, fake_mongodb):
+    db = 'test'
+    collection = 'snapshot'
+    mss = MongoSnapshotService(obj=CodeSnapshot, mongodb=fake_mongodb, db=db, collection=collection)
+    code = Code(add2)
+    snapshot = CodeSnapshot(code)
+    mss.save(snapshot=snapshot)
+    assert fake_mongodb[db][collection].count_documents({}) == 1
+    assert mss.load(snapshot.serial_number) == snapshot.to_dict()
+    loaded = mss.load(snapshot.serial_number, projection={'state'})
+    assert set(loaded.keys()) == {'serial_number', 'state'}
+    assert 'state' in loaded
+    assert loaded['state'] == 'UNKNOWN'
+    assert 'serial_number' in loaded
+    assert loaded['serial_number'] == snapshot.serial_number
+
+
+def test_mongo_code_snapshot_service_search_and_remove(default_os_env, fake_mongodb):
+    db = 'test'
+    collection = 'snapshot'
+    mss = MongoSnapshotService(obj=CodeSnapshot, mongodb=fake_mongodb, db=db, collection=collection)
+    code1 = Code(add2)
+    code2 = Code(add3)
+    code1 >> code2
+    code2(1, 2, 3)
+    snapshot1 = CodeSnapshot(code1)
+    snapshot2 = CodeSnapshot(code2)
+    mss.save(snapshot=snapshot1)
+    mss.save(snapshot=snapshot2)
+    assert fake_mongodb[db][collection].count_documents({}) == 2
+    loaded = mss.load([snapshot1.serial_number, snapshot2.serial_number, '1234'])
+    assert len(loaded) == 2
+    assert loaded[0]['state'] == 'UNKNOWN'
+    assert loaded[1]['state'] == 'WAITING'
+    loaded = mss.load([snapshot1.serial_number, snapshot2.serial_number, '1234'], projection={'state'})
+    for x in loaded:
+        assert set(x.keys()) == {'state', 'serial_number'}
+    search_result = mss.search(key='state', value='WAITING')
+    assert len(search_result) == 1
+    assert search_result[0] == snapshot2.to_dict()
+    search_result = mss.search(key='state', value='WAITING', projection={'state'})
+    assert len(search_result) == 1
+    assert len(search_result[0].keys()) == 2
+    assert search_result[0]['state'] == 'WAITING'
+    assert search_result[0]['serial_number'] == code2.serial_number
+    mss.remove(snapshot2.serial_number)
+    loaded = mss.load([snapshot1.serial_number, snapshot2.serial_number, '1234'])
+    assert len(loaded) == 1
+    search_result = mss.search(key='state', value='WAITING', projection={'state'})
+    assert len(search_result) == 0
+
+
+def test_mongo_snapshot_service_update(fake_mongodb):
+    db = 'test'
+    collection = 'snapshot'
+    mss = MongoSnapshotService(obj=Snapshot, mongodb=fake_mongodb, db=db, collection=collection)
+    timestamp = datetime.now().timestamp()
+    snapshot1 = Snapshot(id='1234', serial_number='5678', timestamp=timestamp)
+    snapshot2 = Snapshot(id='1234', serial_number='5678', timestamp=timestamp + 1)
+    mss.save(snapshot=snapshot1)
+    mss.save(snapshot=snapshot2)
+    assert fake_mongodb[db][collection].count_documents({}) == 1
+    search_result = mss.search(key='id', value='1234', projection={'timestamp'})
+    assert len(search_result) == 1
+    assert search_result[0]['timestamp'] == timestamp + 1
diff --git a/tests/test_state.py b/tests/test_state.py
index 2002ece..be43241 100644
--- a/tests/test_state.py
+++ b/tests/test_state.py
@@ -1,34 +1,21 @@
-from codepack.utils.state import State, StateCode
+from codepack.utils.state import State
 from codepack import Code
 from tests import *
+from codepack.snapshot import Snapshot
 
 
 def test_state_eq():
-    state1 = State(id='test1', serial_number='1234', state='WAITING')
-    state2 = State(id='test2', serial_number='5678', state='RUNNING')
-    state_code = StateCode.WAITING
-    assert state1 == 'WAITING'
-    assert state1 == StateCode.WAITING
-    assert state1 == 3
-    assert state1 == state1.to_dict()
-    assert state1 == State.get_state_code(3)
-    state2.set('WAITING')
-    assert state1 == state2
+    snapshot1 = Snapshot(id='test1', serial_number='1234', state='WAITING')
+    snapshot2 = Snapshot(id='test2', serial_number='5678', state='RUNNING')
+    state_code = State.WAITING
+    assert snapshot1['state'] == 'WAITING'
+    assert snapshot1['state'] == State.WAITING
+    assert snapshot1['state'] == 3
+    snapshot2['state'] = 'WAITING'
+    assert snapshot1['state'] == snapshot2['state']
     assert state_code == 'WAITING'
-    assert state_code == StateCode.WAITING
+    assert state_code == State.WAITING
     assert state_code == 3
-    assert state_code == State.get_state_code(3)
-    assert state_code == state1
-    assert state_code == state1.to_dict()
-
-
-def test_state_dict():
-    state1 = State(id='test', serial_number='1234', state='WAITING')
-    state_dict = {'id': 'test', '_id': '1234', 'state': 'WAITING'}
-    state2 = State.from_dict(state_dict)
-    assert state1.id == state2.id
-    assert state1.serial_number == state2.serial_number
-    assert state1.state == state2.state
 
 
 def test_code_new(default_os_env):
@@ -45,13 +32,13 @@ def test_code_waiting(default_os_env):
     code2.receive('b') << code1
     code2(3, c=2)
     assert code2.get_state() == 'WAITING'
-    state_dict = code2.service['state_manager'].check(code2.serial_number)
-    assert 'args' in state_dict
-    assert isinstance(state_dict['args'], list)
-    assert len(state_dict['args']) == 1
-    assert state_dict['args'][0] == 3
-    assert 'kwargs' in state_dict
-    assert isinstance(state_dict['kwargs'], dict)
-    assert len(state_dict['kwargs']) == 1
-    assert 'c' in state_dict['kwargs'].keys()
-    assert state_dict['kwargs']['c'] == 2
+    snapshot = code2.service['snapshot_service'].load(serial_number=code2.serial_number)
+    assert 'args' in snapshot
+    assert isinstance(snapshot['args'], list)
+    assert len(snapshot['args']) == 1
+    assert snapshot['args'][0] == 3
+    assert 'kwargs' in snapshot
+    assert isinstance(snapshot['kwargs'], dict)
+    assert len(snapshot['kwargs']) == 1
+    assert 'c' in snapshot['kwargs'].keys()
+    assert snapshot['kwargs']['c'] == 2
diff --git a/tests/test_state_manager.py b/tests/test_state_manager.py
deleted file mode 100644
index 0ac3396..0000000
--- a/tests/test_state_manager.py
+++ /dev/null
@@ -1,176 +0,0 @@
-from codepack.service import MemoryStateManager, FileStateManager, MongoStateManager
-from codepack.utils.state import State
-import os
-
-
-def test_singleton_memory_state_manager():
-    msm1 = MemoryStateManager()
-    msm1.init()
-    code_id = 'test'
-    serial_number = '1234'
-    state = 'RUNNING'
-    msm1.set(id=code_id, serial_number=serial_number, state=state)
-    msm2 = MemoryStateManager()
-    assert msm1 == msm2
-    assert len(msm2.states) == 1
-    assert msm2.check(serial_number=serial_number)
-
-
-def test_memory_state_manager_check():
-    msm = MemoryStateManager()
-    msm.init()
-    code_id = 'test'
-    serial_number = '1234'
-    serial_numbers = ['123', '456', '789']
-    state = 'RUNNING'
-    msm.set(id=code_id, serial_number=serial_number, state=state)
-    assert isinstance(msm.check(serial_number=serial_number), dict)
-    msm.remove(serial_number=serial_number)
-    msm.set(id=code_id, serial_number=serial_numbers[0], state=state)
-    msm.set(id=code_id, serial_number=serial_numbers[1], state=state)
-    check = msm.check(serial_number=serial_numbers)
-    assert isinstance(check, list)
-    assert len(check) == 2
-
-
-def test_file_state_manager_check(testdir_state_manager):
-    fsm = FileStateManager(path=testdir_state_manager)
-    code_id = 'test'
-    serial_number = '1234'
-    serial_numbers = ['123', '456', '789']
-    state = 'RUNNING'
-    assert os.path.isdir(testdir_state_manager)
-    fsm.set(id=code_id, serial_number=serial_number, state=state)
-    assert isinstance(fsm.check(serial_number=serial_number), dict)
-    fsm.remove(serial_number=serial_number)
-    fsm.set(id=code_id, serial_number=serial_numbers[0], state=state)
-    fsm.set(id=code_id, serial_number=serial_numbers[1], state=state)
-    check = fsm.check(serial_number=serial_numbers)
-    assert isinstance(check, list)
-    assert len(check) == 2
-
-
-def test_mongo_state_manager_check(fake_mongodb):
-    db = 'test'
-    collection = 'state'
-    msm = MongoStateManager(mongodb=fake_mongodb, db=db, collection=collection)
-    code_id = 'test'
-    serial_number = '1234'
-    serial_numbers = ['123', '456', '789']
-    msm.remove(serial_number=serial_number)
-    for s in serial_numbers:
-        msm.remove(serial_number=s)
-    state = 'RUNNING'
-    msm.set(id=code_id, serial_number=serial_number, state=state)
-    assert isinstance(msm.check(serial_number=serial_number), dict)
-    msm.remove(serial_number=serial_number)
-    msm.set(id=code_id, serial_number=serial_numbers[0], state=state)
-    msm.set(id=code_id, serial_number=serial_numbers[1], state=state)
-    check = msm.check(serial_number=serial_numbers)
-    assert isinstance(check, list)
-    assert len(check) == 2
-
-
-def test_memory_state_manager():
-    msm = MemoryStateManager()
-    msm.init()
-    code_id = 'test'
-    serial_number = '1234'
-    state = 'RUNNING'
-    msm.set(id=code_id, serial_number=serial_number, state=state)
-    assert serial_number in msm.states, "'set' failed"
-    check = msm.check(serial_number=serial_number)
-    get = msm.get(serial_number=serial_number)
-    assert check['id'] == code_id and check['_id'] == serial_number, "'check' failed"
-    assert get == state, "'get' failed"
-    msm.remove(serial_number=serial_number)
-    check2 = msm.check(serial_number=serial_number)
-    assert not check2, "'remove' failed"
-
-
-def test_file_state_manager(testdir_state_manager):
-    fsm = FileStateManager(path=testdir_state_manager)
-    code_id = 'test'
-    serial_number = '1234'
-    state = 'RUNNING'
-    fsm.set(id=code_id, serial_number=serial_number, state=state)
-    assert os.path.isfile(State.get_path(serial_number=serial_number, path=fsm.path)), "'send' failed"
-    check = fsm.check(serial_number=serial_number)
-    get = fsm.get(serial_number=serial_number)
-    assert check['id'] == code_id and check['_id'] == serial_number, "'check' failed"
-    assert get == state, "'get' failed"
-    fsm.remove(serial_number=serial_number)
-    check2 = fsm.check(serial_number=serial_number)
-    assert not check2, "'remove' failed"
-
-
-def test_mongo_state_manager(fake_mongodb):
-    db = 'test'
-    collection = 'state'
-    msm = MongoStateManager(mongodb=fake_mongodb, db=db, collection=collection)
-    code_id = 'test'
-    serial_number = '1234'
-    state = 'RUNNING'
-    msm.set(id=code_id, serial_number=serial_number, state=state)
-    assert fake_mongodb[db][collection].find_one({'_id': serial_number}), "'send' failed"
-    check = msm.check(serial_number=serial_number)
-    get = msm.get(serial_number=serial_number)
-    assert check['id'] == code_id and check['_id'] == serial_number, "'check' failed"
-    assert get == state, "'get' failed"
-    msm.remove(serial_number=serial_number)
-    check2 = msm.check(serial_number=serial_number)
-    assert not check2, "'remove' failed"
-
-
-def test_memory_state_manager_multiple_get_and_search():
-    msm = MemoryStateManager()
-    msm.init()
-    code_ids = ['test1', 'test2']
-    serial_numbers = ['123', '456']
-    state = 'WAITING'
-    msm.set(id=code_ids[0], serial_number=serial_numbers[0], state=state)
-    msm.set(id=code_ids[1], serial_number=serial_numbers[1], state=state)
-    states = msm.get(serial_numbers)
-    search_result = msm.search('WAITING')
-    assert len(states) == 2
-    assert len(search_result) == len(serial_numbers)
-    for s in states:
-        assert s == 'WAITING'
-    for i in range(len(search_result)):
-        assert search_result[i]['_id'] == serial_numbers[i]
-
-
-def test_file_state_manager_multiple_get_and_search(testdir_state_manager):
-    fsm = FileStateManager(path=testdir_state_manager)
-    code_ids = ['test1', 'test2']
-    serial_numbers = ['123', '456']
-    state = 'WAITING'
-    fsm.set(id=code_ids[0], serial_number=serial_numbers[0], state=state)
-    fsm.set(id=code_ids[1], serial_number=serial_numbers[1], state=state)
-    states = fsm.get(serial_numbers)
-    search_result = fsm.search('WAITING')
-    assert len(states) == 2
-    assert len(search_result) == len(serial_numbers)
-    for s in states:
-        assert s == 'WAITING'
-    for i in range(len(search_result)):
-        assert search_result[i]['_id'] == serial_numbers[i]
-
-
-def test_mongo_state_manager_multiple_get_and_search(fake_mongodb):
-    db = 'test'
-    collection = 'state'
-    msm = MongoStateManager(mongodb=fake_mongodb, db=db, collection=collection)
-    code_ids = ['test1', 'test2']
-    serial_numbers = ['123', '456']
-    state = 'WAITING'
-    msm.set(id=code_ids[0], serial_number=serial_numbers[0], state=state)
-    msm.set(id=code_ids[1], serial_number=serial_numbers[1], state=state)
-    states = msm.get(serial_numbers)
-    search_result = msm.search('WAITING')
-    assert len(states) == 2
-    assert len(search_result) == len(serial_numbers)
-    for s in states:
-        assert s == 'WAITING'
-    for i in range(len(search_result)):
-        assert search_result[i]['_id'] == serial_numbers[i]
diff --git a/tests/test_storage_service.py b/tests/test_storage_service.py
index f67d005..ea6f718 100644
--- a/tests/test_storage_service.py
+++ b/tests/test_storage_service.py
@@ -4,17 +4,6 @@
 import os
 
 
-def test_singleton_memory_storage_service(default_os_env):
-    mss1 = MemoryStorageService()
-    mss1.init()
-    code = Code(hello)
-    mss1.save(code)
-    mss2 = MemoryStorageService()
-    assert mss1 == mss2
-    assert len(mss1.storage) == len(mss2.storage) == 1
-    assert mss2.check(id=code.id)
-
-
 def test_memory_storage_service_check(default_os_env):
     mss = MemoryStorageService(obj=Code)
     mss.init()
@@ -22,12 +11,12 @@ def test_memory_storage_service_check(default_os_env):
     code2 = Code(hello, id='hello2', storage_service=mss)
     code3 = Code(hello, id='hello3', storage_service=mss)
     code1.save()
-    assert isinstance(mss.check(id=code1.id), dict)
-    assert code1.id in mss.storage
+    assert isinstance(mss.check(id=code1.id), str)
+    assert code1.id in mss.memory
     mss.remove(id=code1.id)
     code2.save()
     code3.save()
-    assert len(mss.storage) == 2
+    assert len(mss.memory) == 2
     check = mss.check([code1.id, code2.id, code3.id])
     assert isinstance(check, list)
     assert len(check) == 2
@@ -45,12 +34,12 @@ def test_file_storage_service_check(default_os_env, testdir_storage_service):
             continue
     code1.save()
     code2.save()
-    check = [list(x.keys())[0] for x in fss.check([code1.id, code2.id, code3.id])]
+    check = fss.check([code1.id, code2.id, code3.id])
     assert isinstance(check, list)
     assert code1.id in check and code2.id in check and code3.id not in check
     fss.remove(id=code1.id)
     code3.save()
-    check = [list(x.keys())[0] for x in fss.check([code1.id, code2.id, code3.id])]
+    check = fss.check([code1.id, code2.id, code3.id])
     assert isinstance(check, list)
     assert code1.id not in check and code2.id in check and code3.id in check
     assert len(check) == 2
@@ -70,12 +59,12 @@ def test_mongo_storage_service_check(default_os_env, fake_mongodb):
             continue
     code1.save()
     code2.save()
-    check = [list(x.keys())[0] for x in mss.check([code1.id, code2.id, code3.id])]
+    check = mss.check([code1.id, code2.id, code3.id])
     assert isinstance(check, list)
     assert code1.id in check and code2.id in check and code3.id not in check
     mss.remove(id=code1.id)
     code3.save()
-    check = [list(x.keys())[0] for x in mss.check([code1.id, code2.id, code3.id])]
+    check = mss.check([code1.id, code2.id, code3.id])
     assert isinstance(check, list)
     assert code1.id not in check and code2.id in check and code3.id in check
     assert len(check) == 2
@@ -87,15 +76,15 @@ def test_memory_storage_service(default_os_env):
     code1 = Code(hello, storage_service=mss)
     code2 = Code(add2, storage_service=mss)
     code1.save()
-    assert len(mss.storage) == 1
-    assert code1.id in mss.storage
-    assert code2.id not in mss.storage
+    assert len(mss.memory) == 1
+    assert code1.id in mss.memory
+    assert code2.id not in mss.memory
     code3 = mss.load(code1.id)
     assert code1.id == code3.id
     assert code1.source == code3.source
     assert code1("CodePack") == code3("CodePack")
     mss.remove(code3.id)
-    assert len(mss.storage) == 0
+    assert len(mss.memory) == 0
 
 
 def test_file_storage_service(default_os_env, testdir_storage_service):
@@ -105,15 +94,15 @@ def test_file_storage_service(default_os_env, testdir_storage_service):
     code1 = Code(hello, storage_service=fss)
     code2 = Code(add2, storage_service=fss)
     code1.save()
-    assert not os.path.isfile(fss.get_path(id=code1.id))
-    assert os.path.isfile(fss.get_path(id=code1.id, path=filepath))
-    assert not os.path.isfile(fss.get_path(id=code2.id, path=filepath))
+    assert not os.path.isfile(Code.get_path(serial_number=code1.id))
+    assert os.path.isfile(Code.get_path(serial_number=code1.id, path=filepath))
+    assert not os.path.isfile(Code.get_path(serial_number=code2.id, path=filepath))
     code3 = fss.load(code1.id)
     assert code1.id == code3.id
     assert code1.source.strip() == code3.source.strip()
     assert code1("CodePack") == code3("CodePack")
     fss.remove(code3.id)
-    assert not os.path.isfile(fss.get_path(id=code1.id, path=filepath))
+    assert not os.path.isfile(Code.get_path(serial_number=code1.id, path=filepath))
 
 
 def test_mongo_storage_service(default_os_env, fake_mongodb):
diff --git a/tests/test_utils_config.py b/tests/test_utils_config.py
index e3686f2..d02c571 100644
--- a/tests/test_utils_config.py
+++ b/tests/test_utils_config.py
@@ -1,8 +1,9 @@
 from codepack import Code
 from tests import *
-from codepack.service import *
+from codepack.utils.config import get_default_service_config
 import pytest
 import os
+from codepack.service import MemoryDeliveryService, FileSnapshotService, MongoStorageService
 
 
 def test_no_config():
@@ -20,57 +21,57 @@ def test_config_path():
     assert code(1, 2) == 3
 
 
-def test_default_memory_state_manager_with_os_env(default_services):
+def test_default_memory_code_snapshot_service_with_os_env(default_services):
     with pytest.raises(AssertionError):
-        get_default_service_config('state')
-    env_source = 'CODEPACK_STATE_SOURCE'
+        get_default_service_config('code_snapshot')
+    env_source = 'CODEPACK_CODE_SNAPSHOT_SOURCE'
     try:
         os.environ[env_source] = 'MEMORY'
-        msm = default_services.get_default_state_manager()
-        assert hasattr(msm, 'states')
+        mss = default_services.get_default_code_snapshot_service()
+        assert hasattr(mss, 'memory')
     finally:
         if env_source in os.environ:
             os.environ.pop(env_source, None)
 
 
-def test_default_file_state_manager_with_os_env(default_services):
+def test_default_file_code_snapshot_service_with_os_env(default_services):
     with pytest.raises(AssertionError):
-        get_default_service_config('state')
-    env_source = 'CODEPACK_STATE_SOURCE'
-    env_path = 'CODEPACK_STATE_PATH'
+        get_default_service_config('code_snapshot')
+    env_source = 'CODEPACK_CODE_SNAPSHOT_SOURCE'
+    env_path = 'CODEPACK_CODE_SNAPSHOT_PATH'
     try:
         os.environ[env_source] = 'FILE'
         os.environ[env_path] = 'tmp/'
-        fsm = default_services.get_default_state_manager()
-        assert hasattr(fsm, 'path')
-        assert fsm.path == 'tmp/'
+        fss = default_services.get_default_code_snapshot_service()
+        assert hasattr(fss, 'path')
+        assert fss.path == 'tmp/'
     finally:
         for env in [env_source, env_path]:
             if env in os.environ:
                 os.environ.pop(env, None)
 
 
-def test_default_mongodb_state_manager_with_os_env(default_services):
+def test_default_mongo_code_snapshot_service_with_os_env(default_services):
     with pytest.raises(AssertionError):
-        get_default_service_config('state')
-    env_source = 'CODEPACK_STATE_SOURCE'
-    env_db = 'CODEPACK_STATE_DB'
-    env_collection = 'CODEPACK_STATE_COLLECTION'
+        get_default_service_config('code_snapshot')
+    env_source = 'CODEPACK_CODE_SNAPSHOT_SOURCE'
+    env_db = 'CODEPACK_CODE_SNAPSHOT_DB'
+    env_collection = 'CODEPACK_CODE_SNAPSHOT_COLLECTION'
     env_config_path = 'CODEPACK_CONN_PATH'
-    msm = None
+    mss = None
     try:
         os.environ[env_source] = 'MONGODB'
         os.environ[env_db] = 'test'
-        os.environ[env_collection] = 'state'
+        os.environ[env_collection] = 'snapshot'
         os.environ[env_config_path] = 'config/test_conn.ini'
-        msm = default_services.get_default_state_manager()
-        assert hasattr(msm, 'mongodb')
+        mss = default_services.get_default_code_snapshot_service()
+        assert hasattr(mss, 'mongodb')
     finally:
         for env in [env_source, env_db, env_collection, env_config_path]:
             if env in os.environ:
                 os.environ.pop(env, None)
-        if msm is not None and not msm.mongodb.closed:
-            msm.mongodb.close()
+        if mss is not None and not mss.mongodb.closed:
+            mss.mongodb.close()
 
 
 def test_default_memory_delivery_service_with_os_env(default_services):
@@ -80,7 +81,7 @@ def test_default_memory_delivery_service_with_os_env(default_services):
     try:
         os.environ[env_source] = 'MEMORY'
         mds = default_services.get_default_delivery_service()
-        assert hasattr(mds, 'deliveries')
+        assert hasattr(mds, 'memory')
     finally:
         if env_source in os.environ:
             os.environ.pop(env_source, None)
@@ -103,7 +104,7 @@ def test_default_file_delivery_service_with_os_env(default_services):
                 os.environ.pop(env, None)
 
 
-def test_default_mongodb_delivery_service_with_os_env(default_services):
+def test_default_mongo_delivery_service_with_os_env(default_services):
     with pytest.raises(AssertionError):
         get_default_service_config('cache')
     env_source = 'CODEPACK_CACHE_SOURCE'
@@ -129,11 +130,11 @@ def test_default_mongodb_delivery_service_with_os_env(default_services):
 def test_default_memory_code_storage_service_with_os_env(default_services):
     with pytest.raises(AssertionError):
         get_default_service_config('code')
-    env_source = 'CODEPACK_CODE_SOURCE'
+    env_source = 'CODEPACK_CODE_STORAGE_SOURCE'
     try:
         os.environ[env_source] = 'MEMORY'
         mss = default_services.get_default_code_storage_service(obj=Code)
-        assert hasattr(mss, 'storage')
+        assert hasattr(mss, 'memory')
         assert mss.obj == Code
     finally:
         if env_source in os.environ:
@@ -143,8 +144,8 @@ def test_default_memory_code_storage_service_with_os_env(default_services):
 def test_default_file_code_storage_service_with_os_env(default_services):
     with pytest.raises(AssertionError):
         get_default_service_config('cache')
-    env_source = 'CODEPACK_CODE_SOURCE'
-    env_path = 'CODEPACK_CODE_PATH'
+    env_source = 'CODEPACK_CODE_STORAGE_SOURCE'
+    env_path = 'CODEPACK_CODE_STORAGE_PATH'
     try:
         os.environ[env_source] = 'FILE'
         os.environ[env_path] = 'tmp/'
@@ -158,12 +159,12 @@ def test_default_file_code_storage_service_with_os_env(default_services):
                 os.environ.pop(env, None)
 
 
-def test_default_mongodb_code_storage_service_with_os_env(default_services):
+def test_default_mongo_code_storage_service_with_os_env(default_services):
     with pytest.raises(AssertionError):
         get_default_service_config('cache')
-    env_source = 'CODEPACK_CODE_SOURCE'
-    env_db = 'CODEPACK_CODE_DB'
-    env_collection = 'CODEPACK_CODE_COLLECTION'
+    env_source = 'CODEPACK_CODE_STORAGE_SOURCE'
+    env_db = 'CODEPACK_CODE_STORAGE_DB'
+    env_collection = 'CODEPACK_CODE_STORAGE_COLLECTION'
     env_config_path = 'CODEPACK_CONN_PATH'
     mss = None
     try:
@@ -182,37 +183,34 @@ def test_default_mongodb_code_storage_service_with_os_env(default_services):
             mss.mongodb.close()
 
 
-def test_if_default_services_have_single_instance_for_each_service(default_services, testdir_state_manager):
+def test_if_default_services_have_single_instance_for_each_service(default_services, testdir_snapshot_service):
     os.environ['CODEPACK_CACHE_SOURCE'] = 'MEMORY'
-
-    os.environ['CODEPACK_STATE_SOURCE'] = 'FILE'
-    os.environ['CODEPACK_STATE_PATH'] = testdir_state_manager
-
-    os.environ['CODEPACK_CODE_SOURCE'] = 'MONGODB'
-    os.environ['CODEPACK_CODE_DB'] = 'test'
-    os.environ['CODEPACK_CODE_COLLECTION'] = 'codes'
+    os.environ['CODEPACK_CODE_SNAPSHOT_SOURCE'] = 'FILE'
+    os.environ['CODEPACK_CODE_SNAPSHOT_PATH'] = testdir_snapshot_service
+    os.environ['CODEPACK_CODE_STORAGE_SOURCE'] = 'MONGODB'
+    os.environ['CODEPACK_CODE_STORAGE_DB'] = 'test'
+    os.environ['CODEPACK_CODE_STORAGE_COLLECTION'] = 'codes'
     os.environ['CODEPACK_CONN_PATH'] = 'config/test_conn.ini'
-
     try:
         code1 = Code(add2)
         code2 = Code(add3)
-
         assert isinstance(code1.service['delivery_service'], MemoryDeliveryService)
-        assert isinstance(code1.service['state_manager'], FileStateManager)
+        assert isinstance(code1.service['snapshot_service'], FileSnapshotService)
         assert isinstance(code1.service['storage_service'], MongoStorageService)
         assert isinstance(code2.service['delivery_service'], MemoryDeliveryService)
-        assert isinstance(code2.service['state_manager'], FileStateManager)
+        assert isinstance(code2.service['snapshot_service'], FileSnapshotService)
         assert isinstance(code2.service['storage_service'], MongoStorageService)
 
         assert (code1.service['delivery_service']) == (code2.service['delivery_service'])
-        assert (code1.service['state_manager']) == (code2.service['state_manager'])
+        assert (code1.service['snapshot_service']) == (code2.service['snapshot_service'])
         assert (code1.service['storage_service']) == (code2.service['storage_service'])
 
         assert id(code1.service['delivery_service']) == id(code2.service['delivery_service'])
-        assert id(code1.service['state_manager']) == id(code2.service['state_manager'])
+        assert id(code1.service['snapshot_service']) == id(code2.service['snapshot_service'])
         assert id(code1.service['storage_service']) == id(code2.service['storage_service'])
     finally:
         for env in ['CODEPACK_CACHE_SOURCE',
-                    'CODEPACK_STATE_SOURCE', 'CODEPACK_STATE_PATH',
-                    'CODEPACK_CODE_SOURCE', 'CODEPACK_CODE_DB', 'CODEPACK_CODE_COLLECTION', 'CODEPACK_CONN_PATH']:
+                    'CODEPACK_CODE_SNAPSHOT_SOURCE', 'CODEPACK_CODE_SNAPSHOT_PATH',
+                    'CODEPACK_CODE_STORAGE_SOURCE', 'CODEPACK_CODE_STORAGE_DB', 'CODEPACK_CODE_STORAGE_COLLECTION',
+                    'CODEPACK_CONN_PATH']:
             os.environ.pop(env, None)