| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with this |
| * work for additional information regarding copyright ownership. The ASF |
| * licenses this file to You under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package org.apache.hugegraph.backend.store; |
| |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.apache.hugegraph.HugeException; |
| import org.apache.hugegraph.backend.id.Id; |
| import org.apache.hugegraph.iterator.ExtendableIterator; |
| import org.apache.hugegraph.perf.PerfUtil.Watched; |
| import org.apache.hugegraph.type.HugeType; |
| import org.apache.hugegraph.type.define.Action; |
| import org.apache.hugegraph.util.E; |
| import org.apache.hugegraph.util.InsertionOrderUtil; |
| |
| public class BackendMutation { |
| |
| private final MutationTable updates; |
| |
| public BackendMutation() { |
| this.updates = new MutationTable(); |
| } |
| |
| public BackendMutation(int initialCapacity) { |
| this.updates = new MutationTable(initialCapacity); |
| } |
| |
| /** |
| * Add data entry with an action to collection `updates` |
| * @param entry the backend entry |
| * @param action operate action on the entry |
| */ |
| @Watched(prefix = "mutation") |
| public void add(BackendEntry entry, Action action) { |
| Id id = entry.id(); |
| assert id != null; |
| if (this.updates.containsKey(entry.type(), id)) { |
| this.optimizeUpdates(entry, action); |
| } else { |
| // If there is no entity with this id, add it |
| this.updates.put(entry.type(), id, BackendAction.of(action, entry)); |
| } |
| } |
| |
| /** |
| * Put directly without checking and merging |
| */ |
| public void put(BackendEntry entry, Action action) { |
| Id id = entry.id(); |
| this.updates.put(entry.type(), id, BackendAction.of(action, entry)); |
| } |
| |
| /** |
| * The optimized scenes include but are not limited to: |
| * 1.If you want to delete an entry, the other mutations previously |
| * can be ignored. |
| * 2.As similar to the No.1 item, If you want to insert an entry, |
| * the other mutations previously also can be ignored. |
| * 3.If you append an entry and then eliminate it, the new action |
| * can override the old one. |
| */ |
| @Watched(prefix = "mutation") |
| private void optimizeUpdates(BackendEntry entry, Action action) { |
| final Id id = entry.id(); |
| assert id != null; |
| final List<BackendAction> items = this.updates.get(entry.type(), id); |
| assert items != null; |
| boolean ignoreCurrent = false; |
| for (Iterator<BackendAction> iter = items.iterator(); iter.hasNext();) { |
| BackendAction originItem = iter.next(); |
| Action originAction = originItem.action(); |
| switch (action) { |
| case INSERT: |
| iter.remove(); |
| break; |
| case DELETE: |
| if (originAction == Action.INSERT) { |
| throw incompatibleActionException(action, originAction); |
| } else if (originAction == Action.DELETE) { |
| ignoreCurrent = true; |
| } else { |
| iter.remove(); |
| } |
| break; |
| case APPEND: |
| if (entry.type().isUniqueIndex() && |
| originAction == Action.APPEND) { |
| throw new IllegalArgumentException(String.format( |
| "Unique constraint conflict is found in" + |
| " transaction between %s and %s", |
| entry, originItem.entry())); |
| } |
| |
| if (originAction == Action.INSERT || |
| originAction == Action.DELETE) { |
| throw incompatibleActionException(action, originAction); |
| } else { |
| Id subId = entry.subId(); |
| Id originSubId = originItem.entry().subId(); |
| assert subId != null; |
| if (subId == originSubId || subId.equals(originSubId)) { |
| iter.remove(); |
| } |
| } |
| break; |
| case ELIMINATE: |
| if (originAction == Action.INSERT || |
| originAction == Action.DELETE) { |
| throw incompatibleActionException(action, originAction); |
| } else { |
| Id subId = entry.subId(); |
| Id originSubId = originItem.entry().subId(); |
| assert subId != null; |
| if (subId == originSubId || subId.equals(originSubId)) { |
| iter.remove(); |
| } |
| } |
| break; |
| default: |
| throw new AssertionError(String.format( |
| "Unknown mutate action: %s", action)); |
| } |
| } |
| if (!ignoreCurrent) { |
| items.add(BackendAction.of(action, entry)); |
| } |
| } |
| |
| private static HugeException incompatibleActionException( |
| Action newAction, |
| Action originAction) { |
| return new HugeException("The action '%s' is incompatible with " + |
| "action '%s'", newAction, originAction); |
| } |
| |
| /** |
| * Merges another mutation into this mutation. Ensures that all additions |
| * and deletions are added to this mutation. Does not remove duplicates |
| * if such exist - this needs to be ensured by the caller. |
| * @param mutation another mutation to be merged |
| */ |
| public void merge(BackendMutation mutation) { |
| E.checkNotNull(mutation, "mutation"); |
| for (Iterator<BackendAction> it = mutation.mutation(); it.hasNext();) { |
| BackendAction item = it.next(); |
| this.add(item.entry(), item.action()); |
| } |
| } |
| |
| public Set<HugeType> types() { |
| return this.updates.keys(); |
| } |
| |
| /** |
| * Get all mutations |
| * @return mutations |
| */ |
| public Iterator<BackendAction> mutation() { |
| return this.updates.values(); |
| } |
| |
| /** |
| * Get mutations by type |
| * @param type entry type |
| * @return mutations |
| */ |
| public Iterator<BackendAction> mutation(HugeType type) { |
| return this.updates.get(type); |
| } |
| |
| /** |
| * Get mutations by type and id |
| * @param type entry type |
| * @param id entry id |
| * @return mutations |
| */ |
| public List<BackendAction> mutation(HugeType type, Id id) { |
| return this.updates.get(type, id); |
| } |
| |
| /** |
| * Whether mutation contains entry and action |
| * @param entry entry |
| * @param action action |
| * @return true if exist, otherwise false |
| */ |
| public boolean contains(BackendEntry entry, Action action) { |
| List<BackendAction> items = this.updates.get(entry.type(), entry.id()); |
| if (items == null || items.isEmpty()) { |
| return false; |
| } |
| for (BackendAction item : items) { |
| if (item.action().equals(action)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Whether mutation contains type and action |
| * @param type type |
| * @param action action |
| * @return true if exist, otherwise false |
| */ |
| public boolean contains(HugeType type, Action action) { |
| for (Iterator<BackendAction> i = this.updates.get(type); i.hasNext();) { |
| BackendAction entry = i.next(); |
| if (entry.action() == action) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Whether this mutation is empty |
| * @return true if empty, otherwise false |
| */ |
| public boolean isEmpty() { |
| return this.updates.size() == 0; |
| } |
| |
| /** |
| * Get size of mutations |
| * @return size |
| */ |
| public int size() { |
| return this.updates.size(); |
| } |
| |
| public void clear() { |
| this.updates.clear(); |
| } |
| |
| @Override |
| public String toString() { |
| return String.format("BackendMutation{mutations=%s}", this.updates); |
| } |
| |
| private static class MutationTable { |
| |
| // Mapping type => id => mutations |
| private final Map<HugeType, Map<Id, List<BackendAction>>> mutations; |
| |
| public MutationTable() { |
| // NOTE: ensure insert order |
| this.mutations = InsertionOrderUtil.newMap(); |
| } |
| |
| public MutationTable(int initialCapacity) { |
| // NOTE: ensure insert order |
| this.mutations = InsertionOrderUtil.newMap(initialCapacity); |
| } |
| |
| public void put(HugeType type, Id id, BackendAction mutation) { |
| Map<Id, List<BackendAction>> table = this.mutations.get(type); |
| if (table == null) { |
| table = InsertionOrderUtil.newMap(); |
| this.mutations.put(type, table); |
| } |
| |
| List<BackendAction> items = table.get(id); |
| if (items == null) { |
| items = new ArrayList<>(); |
| table.put(id, items); |
| } |
| |
| items.add(mutation); |
| } |
| |
| public boolean containsKey(HugeType type, Id id) { |
| Map<Id, List<BackendAction>> table = this.mutations.get(type); |
| return table != null && table.containsKey(id); |
| } |
| |
| public List<BackendAction> get(HugeType type, Id id) { |
| Map<Id, List<BackendAction>> table = this.mutations.get(type); |
| if (table == null) { |
| return null; |
| } |
| return table.get(id); |
| } |
| |
| public Iterator<BackendAction> get(HugeType type) { |
| ExtendableIterator<BackendAction> rs = new ExtendableIterator<>(); |
| Map<Id, List<BackendAction>> table = this.mutations.get(type); |
| if (table != null) { |
| for (List<BackendAction> items : table.values()) { |
| rs.extend(items.iterator()); |
| } |
| } |
| return rs; |
| } |
| |
| public Set<HugeType> keys() { |
| return this.mutations.keySet(); |
| } |
| |
| public Iterator<BackendAction> values() { |
| ExtendableIterator<BackendAction> rs = new ExtendableIterator<>(); |
| for (Map<Id, List<BackendAction>> table : this.mutations.values()) { |
| for (List<BackendAction> items : table.values()) { |
| rs.extend(items.iterator()); |
| } |
| } |
| return rs; |
| } |
| |
| public int size() { |
| int size = 0; |
| for (Map<Id, List<BackendAction>> m : this.mutations.values()) { |
| // NOTE: Index entry has same id with different subIds |
| for (List<BackendAction> actions : m.values()) { |
| size += actions.size(); |
| } |
| } |
| return size; |
| } |
| |
| public void clear() { |
| this.mutations.clear(); |
| } |
| } |
| } |