@Override
    public Void call() throws Exception {

      boolean didRun = false;
      try {

        acceptSolutions();

        if (op.runHashJoin(context, state)) {

          didRun = true;

          doHashJoin();
        }

        // Done.
        return null;

      } finally {

        if (didRun) {

          /*
           * The state needs to be released each time this operator
           * runs in order to discard the intermediate solutions
           * buffered on the hash index that were just joined against
           * the access path. If we do not discard the state after
           * processing the intermediate solutions, then they will
           * continue to accumulate and we will over-report joins
           * (duplicate solutions will be output for things already in
           * the hash index the next time we evaluate the hash join
           * against the access path).
           */

          state.release();
        }

        sink.close();

        if (sink2 != null) sink2.close();
      }
    }
    /** Do a hash join of the buffered solutions with the access path. */
    private void doHashJoin() {

      if (state.isEmpty()) return;

      final IBindingSetAccessPath<?> accessPath = getAccessPath();

      if (log.isInfoEnabled()) log.info("accessPath=" + accessPath);

      stats.accessPathCount.increment();

      stats.accessPathRangeCount.add(accessPath.rangeCount(false /* exact */));

      final UnsyncLocalOutputBuffer<IBindingSet> unsyncBuffer =
          new UnsyncLocalOutputBuffer<IBindingSet>(op.getChunkCapacity(), sink);

      final long cutoffLimit =
          pred.getProperty(
              IPredicate.Annotations.CUTOFF_LIMIT, IPredicate.Annotations.DEFAULT_CUTOFF_LIMIT);

      // Obtain the iterator for the current join dimension.
      final ICloseableIterator<IBindingSet[]> itr = accessPath.solutions(cutoffLimit, stats);

      /*
       * Note: The [stats] are NOT passed in here since the chunksIn and
       * unitsIn were updated when the pipeline solutions were accepted
       * into the hash index. If we passed in stats here, they would be
       * double counted when we executed the hash join against the access
       * path.
       */
      state.hashJoin(
          itr, // left
          null, // stats
          unsyncBuffer // out
          );

      switch (state.getJoinType()) {
        case Normal:
          /*
           * Nothing to do.
           */
          break;
        case Optional:
        case NotExists:
          {
            /*
             * Output the optional solutions.
             */

            // where to write the optional solutions.
            final AbstractUnsynchronizedArrayBuffer<IBindingSet> unsyncBuffer2 =
                sink2 == null
                    ? unsyncBuffer
                    : new UnsyncLocalOutputBuffer<IBindingSet>(op.getChunkCapacity(), sink2);

            state.outputOptionals(unsyncBuffer2);

            unsyncBuffer2.flush();
            if (sink2 != null) sink2.flush();

            break;
          }
        case Exists:
          {
            /*
             * Output the join set.
             */
            state.outputJoinSet(unsyncBuffer);
            break;
          }
        default:
          throw new AssertionError();
      }

      unsyncBuffer.flush();
      sink.flush();
    }
    /** Buffer intermediate resources. */
    private void acceptSolutions() {

      state.acceptSolutions(context.getSource(), stats);
    }