001/*
002 * (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.core.storage.sql;
020
021import java.io.Serializable;
022import java.util.ArrayList;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028
029import org.apache.commons.collections.map.AbstractReferenceMap;
030import org.apache.commons.collections.map.ReferenceMap;
031import org.nuxeo.runtime.metrics.MetricsService;
032
033import io.dropwizard.metrics5.Counter;
034import io.dropwizard.metrics5.MetricName;
035import io.dropwizard.metrics5.MetricRegistry;
036import io.dropwizard.metrics5.SharedMetricRegistries;
037import io.dropwizard.metrics5.Timer;
038
039/**
040 * A {@link SelectionContext} holds information for a set {@link Selection} objects, mostly acting as a cache.
041 * <p>
042 * Some of the information is identical to what's in the database and can be safely be GC'ed, so it lives in a
043 * memory-sensitive map (softMap), otherwise it's moved to a normal map (hardMap) (creation or deletion).
044 */
045public class SelectionContext {
046
047    private final SelectionType selType;
048
049    private final Serializable criterion;
050
051    private final RowMapper mapper;
052
053    private final PersistenceContext context;
054
055    private final Map<Serializable, Selection> softMap;
056
057    // public because used from unit tests
058    public final Map<Serializable, Selection> hardMap;
059
060    /**
061     * The selections modified in the transaction, that should be propagated as invalidations to other sessions at
062     * post-commit time.
063     */
064    private final Set<Serializable> modifiedInTransaction;
065
066    // @since 5.7
067    protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName());
068
069    protected final Counter modifiedInTransactionCount;
070
071    protected final Counter cacheHitCount;
072
073    protected final Timer cacheGetTimer;
074
075    @SuppressWarnings("unchecked")
076    public SelectionContext(SelectionType selType, Serializable criterion, RowMapper mapper, PersistenceContext context) {
077        this.selType = selType;
078        this.criterion = criterion;
079        this.mapper = mapper;
080        this.context = context;
081        softMap = new ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.SOFT);
082        hardMap = new HashMap<>();
083        modifiedInTransaction = new HashSet<>();
084        modifiedInTransactionCount = registry.counter(
085                MetricName.build("nuxeo", "repositories", "repository", "cache", "selections", "modified")
086                          .tagged("repository", context.session.repository.getName()));
087        cacheHitCount = registry.counter(
088                MetricName.build("nuxeo", "repositories", "repository", "cache", "selections", "hit")
089                          .tagged("repository", context.session.repository.getName()));
090        cacheGetTimer = registry.timer(
091                MetricName.build("nuxeo", "repositories", "repository", "cache", "selections", "timer")
092                          .tagged("repository", context.session.repository.getName()));
093    }
094
095    public int clearCaches() {
096        // only the soft selections are caches, the others hold info
097        int n = softMap.size();
098        softMap.clear();
099        modifiedInTransactionCount.dec(modifiedInTransaction.size());
100        modifiedInTransaction.clear();
101        return n;
102    }
103
104    public int getSize() {
105        return softMap == null ? 0 : softMap.size();
106    }
107
108    /** Gets the proper selection cache. Creates one if missing. */
109    private Selection getSelection(Serializable selId) {
110        Selection selection = getSelectionOrNull(selId);
111        if (selection != null) {
112            return selection;
113        }
114        return new Selection(selId, selType.tableName, false, selType.filterKey, context, softMap, hardMap);
115    }
116
117    /**
118     * Gets the proper selection cache, if it exists, otherwise returns {@code null}.
119     *
120     * @since 9.2
121     */
122    @SuppressWarnings("resource") // Time.Context closed by stop()
123    protected Selection getSelectionOrNull(Serializable selId) {
124        final Timer.Context timerContext = cacheGetTimer.time();
125        try {
126            Selection selection = softMap.get(selId);
127            if (selection != null) {
128                cacheHitCount.inc();
129                return selection;
130            }
131            selection = hardMap.get(selId);
132            if (selection != null) {
133                cacheHitCount.inc();
134                return selection;
135            }
136        } finally {
137            timerContext.stop();
138        }
139        return null;
140    }
141
142    public boolean applicable(SimpleFragment fragment) {
143        // check table name
144        if (!fragment.row.tableName.equals(selType.tableName)) {
145            return false;
146        }
147        // check criterion if there's one
148        if (selType.criterionKey != null) {
149            Serializable crit = fragment.get(selType.criterionKey);
150            if (!criterion.equals(crit)) {
151                return false;
152            }
153        }
154        return true;
155    }
156
157    /**
158     * Records the fragment as a just-created selection member.
159     */
160    public void recordCreated(SimpleFragment fragment) {
161        Serializable id = fragment.getId();
162        // add as a new fragment in the selection
163        Serializable selId = fragment.get(selType.selKey);
164        if (selId != null) {
165            getSelection(selId).addCreated(id);
166            modifiedInTransaction.add(selId);
167            modifiedInTransactionCount.inc();
168        }
169    }
170
171    /**
172     * Notes that a new empty selection should be created.
173     */
174    public void newSelection(Serializable selId) {
175        new Selection(selId, selType.tableName, true, selType.filterKey, context, softMap, hardMap);
176    }
177
178    /**
179     * @param invalidate {@code true} if this is for a fragment newly created by internal database process (copy, etc.)
180     *            and must notified to other session; {@code false} if this is a normal read
181     */
182    public void recordExisting(SimpleFragment fragment, boolean invalidate) {
183        Serializable selId = fragment.get(selType.selKey);
184        if (selId != null) {
185            getSelection(selId).addExisting(fragment.getId());
186            if (invalidate) {
187                modifiedInTransaction.add(selId);
188                modifiedInTransactionCount.inc();
189            }
190        }
191    }
192
193    /** Removes a selection item from the selection. */
194    public void recordRemoved(SimpleFragment fragment) {
195        recordRemoved(fragment.getId(), fragment.get(selType.selKey));
196    }
197
198    /** Removes a selection item from the selection. */
199    public void recordRemoved(Serializable id, Serializable selId) {
200        if (selId != null) {
201            getSelection(selId).remove(id);
202            modifiedInTransaction.add(selId);
203            modifiedInTransactionCount.inc();
204        }
205    }
206
207    /** Records a selection as removed. */
208    public void recordRemovedSelection(Serializable selId) {
209        softMap.remove(selId);
210        hardMap.remove(selId);
211        modifiedInTransaction.add(selId);
212        modifiedInTransactionCount.inc();
213    }
214
215    /**
216     * Find a fragment given its selection id and value.
217     * <p>
218     * If the fragment is not in the context, fetch it from the mapper.
219     *
220     * @param selId the selection id
221     * @param filter the value to filter on
222     * @return the fragment, or {@code null} if not found
223     */
224    public SimpleFragment getSelectionFragment(Serializable selId, String filter) {
225        SimpleFragment fragment = getSelection(selId).getFragmentByValue(filter);
226        if (fragment == SimpleFragment.UNKNOWN) {
227            // read it through the mapper
228            List<Row> rows = mapper.readSelectionRows(selType, selId, filter, criterion, true);
229            Row row = rows.isEmpty() ? null : rows.get(0);
230            fragment = (SimpleFragment) context.getFragmentFromFetchedRow(row, false);
231        }
232        return fragment;
233    }
234
235    /**
236     * Finds all the selection fragments for a given id.
237     * <p>
238     * No sorting on value is done.
239     *
240     * @param selId the selection id
241     * @param filter the value to filter on, or {@code null} for all
242     * @return the list of fragments
243     */
244    public List<SimpleFragment> getSelectionFragments(Serializable selId, String filter) {
245        Selection selection = getSelection(selId);
246        List<SimpleFragment> fragments = selection.getFragmentsByValue(filter);
247        if (fragments == null) {
248            // no complete list is known
249            // ask the actual selection to the mapper
250            List<Row> rows = mapper.readSelectionRows(selType, selId, null, criterion, false);
251            List<Fragment> frags = context.getFragmentsFromFetchedRows(rows, false);
252            fragments = new ArrayList<>(frags.size());
253            List<Serializable> ids = new ArrayList<>(frags.size());
254            for (Fragment fragment : frags) {
255                fragments.add((SimpleFragment) fragment);
256                ids.add(fragment.getId());
257            }
258            selection.addExistingComplete(ids);
259
260            // redo the query, as the selection may include newly-created ones,
261            // and we also filter by name
262            fragments = selection.getFragmentsByValue(filter);
263        }
264        return fragments;
265    }
266
267    /**
268     * Gets all the selection fragment ids for a given list of values.
269     *
270     * @since 9.2
271     */
272    public Set<Serializable> getSelectionIds(List<Serializable> values) {
273        return mapper.readSelectionsIds(selType, values);
274    }
275
276    public void postSave() {
277        // flush selection caches (moves from hard to soft)
278        for (Selection selection : hardMap.values()) {
279            selection.flush(); // added to soft map
280        }
281        hardMap.clear();
282    }
283
284    /**
285     * Marks locally all the invalidations gathered by a {@link Mapper} operation (like a version restore).
286     */
287    public void markInvalidated(Set<RowId> modified) {
288        for (RowId rowId : modified) {
289            if (selType.invalidationTableName.equals(rowId.tableName)) {
290                Serializable id = rowId.id;
291                Selection selection = softMap.get(id);
292                if (selection != null) {
293                    selection.setIncomplete();
294                }
295                selection = hardMap.get(id);
296                if (selection != null) {
297                    selection.setIncomplete();
298                }
299                modifiedInTransaction.add(id);
300                modifiedInTransactionCount.inc();
301            }
302        }
303    }
304
305    /**
306     * Gathers invalidations from this session.
307     * <p>
308     * Called post-transaction to gathers invalidations to be sent to others.
309     */
310    public void gatherInvalidations(VCSInvalidations invalidations) {
311        for (Serializable id : modifiedInTransaction) {
312            invalidations.addModified(new RowId(selType.invalidationTableName, id));
313        }
314        modifiedInTransactionCount.dec(modifiedInTransaction.size());
315        modifiedInTransaction.clear();
316    }
317
318    /**
319     * Processes all invalidations accumulated.
320     * <p>
321     * Called pre-transaction.
322     */
323    public void processReceivedInvalidations(Set<RowId> modified) {
324        for (RowId rowId : modified) {
325            if (selType.invalidationTableName.equals(rowId.tableName)) {
326                Serializable id = rowId.id;
327                softMap.remove(id);
328                hardMap.remove(id);
329            }
330        }
331    }
332
333}