@Override
  public int writeToRecord(final int recordId,
                           final @NotNull ByteBufferWriter writer,
                           final int expectedRecordSizeHint,
                           final boolean leaveRedirectOnRecordRelocation) throws IOException {
    //insert new record?
    if (!isValidRecordId(recordId)) {
      final ByteBuffer temp = acquireTemporaryBuffer(expectedRecordSizeHint);
      try {
        final ByteBuffer bufferWithData = writer.write(temp);
        bufferWithData.flip();

        final int recordLength = bufferWithData.limit();
        checkLengthHardLimit(recordLength);
        if (recordLength > maxCapacityForPageSize) {
          throw new IllegalStateException(
            "recordLength(=" + recordLength + ") > maxCapacityForPageSize(=" + maxCapacityForPageSize + ") -- can't fit");
        }

        final int capacity = bufferWithData.capacity();
        //Don't check capacity right here -- let allocation strategy first decide how to deal with capacity > MAX
        final int requestedRecordCapacity = allocationStrategy.capacity(
          recordLength,
          capacity
        );
        if (requestedRecordCapacity < recordLength) {
          throw new IllegalStateException(
            "Allocation strategy " + allocationStrategy + "(" + recordLength + ", " + capacity + ")" +
            " returns " + requestedRecordCapacity + " < length(=" + recordLength + ")");
        }

        return writeToNewlyAllocatedRecord(bufferWithData, requestedRecordCapacity);
      }
      finally {
        releaseTemporaryBuffer(temp);
      }
    }

    //already existent record
    int currentRecordId = recordId;
    for (int i = 0; i < MAX_REDIRECTS; i++) {
      final long recordOffset = idToOffset(currentRecordId);
      final int offsetOnPage = pagedStorage.toOffsetInPage(recordOffset);
      try (final PageUnsafe page = (PageUnsafe)pagedStorage.pageByOffset(recordOffset, /*forWrite: */ true)) {
        page.lockPageForWrite();
        try {
          final ByteBuffer buffer = page.rawPageBuffer();
          final RecordLayout recordLayout = RecordLayout.recordLayout(buffer, offsetOnPage);
          final byte recordType = recordLayout.recordType();
          if (recordType == RecordLayout.RECORD_TYPE_MOVED) {
            final int redirectToId = recordLayout.redirectToId(buffer, offsetOnPage);
            checkRedirectToId(recordId, currentRecordId, redirectToId);
            currentRecordId = redirectToId;
            continue;//hope redirect chains are not too long...
          }
          if (recordType != RecordLayout.RECORD_TYPE_ACTUAL) {
            throw new AssertionError("RecordType(" + recordType + ") should not appear in the chain: " +
                                     "it is either not implemented yet, or all wrong");
          }
          final int recordCapacity = recordLayout.capacity(buffer, offsetOnPage);
          final int recordActualLength = recordLayout.length(buffer, offsetOnPage);
          //TODO RC: consider 'expectedRecordSizeHint' here? I.e. if expectedRecordSizeHint>record.capacity -> allocate heap buffer
          //         of the size asked, copy actual record content into it?
          final int recordPayloadOffset = offsetOnPage + recordLayout.headerSize();
          final ByteBuffer recordContent = buffer.slice(recordPayloadOffset, recordCapacity)
            .limit(recordActualLength)
            .order(buffer.order());

          final ByteBuffer newRecordContent = writer.write(recordContent);
          if (newRecordContent == null) {
            //returned null means writer decides to skip write -> just return current recordId
            return currentRecordId;
          }

          if (newRecordContent != recordContent) {//writer decides to allocate new buffer for content:
            newRecordContent.flip();
            final int newRecordLength = newRecordContent.remaining();
            if (newRecordLength <= recordCapacity) {
              //RC: really, in this case writer should just write data right in the 'recordContent'
              //    buffer, not allocate the new buffer -- but ok, we could deal with it:
              recordLayout.putRecord(buffer, offsetOnPage,
                                     recordCapacity, newRecordLength, NULL_ID, newRecordContent);
              page.regionModified(offsetOnPage, recordLayout.headerSize() + newRecordLength);

              totalLiveRecordsPayloadBytes.addAndGet(newRecordLength - recordActualLength);
            }
            else {//current record is too small for new content -> relocate to a new place
              //FIXME RC: to not hold >1 pagelock at a time, we need to allocate _slot_ for the new record
              //          and write its id into current record -- and then release current page, and acquire
              //          target page, and write data into a new record?
              int newRecordCapacity = allocationStrategy.capacity(newRecordLength, newRecordContent.capacity());
              final int newRecordId = writeToNewlyAllocatedRecord(newRecordContent, newRecordCapacity);

              final RecordLayout.MovedRecord movedRecordLayout = RecordLayout.MovedRecord.INSTANCE;
              //mark current record as either 'moved' or 'deleted'
              final int redirectToId = leaveRedirectOnRecordRelocation ? newRecordId : NULL_ID;
              //Total space occupied by record must remain constant, but record capacity should be
              // changed since MovedRecord has another headerSize than Small|LargeRecord
              final int movedRecordCapacity = recordLayout.fullRecordSize(recordCapacity) - movedRecordLayout.headerSize();
              movedRecordLayout.putRecord(buffer, offsetOnPage, movedRecordCapacity, 0, redirectToId, null);

              page.regionModified(offsetOnPage, movedRecordLayout.headerSize());

              totalLiveRecordsPayloadBytes.addAndGet(-recordActualLength);
              totalLiveRecordsCapacityBytes.addAndGet(-recordCapacity);
              if (leaveRedirectOnRecordRelocation) {
                recordsRelocated.incrementAndGet();
              }
              else {
                recordsDeleted.incrementAndGet();
              }

              return newRecordId;
            }
          }
          else {//if newRecordContent is null or == recordContent -> changes are already written by writer into the recordContent,
            // we only need to adjust record header:
            recordContent.flip();
            final int newRecordLength = recordContent.remaining();
            assert (newRecordLength <= recordCapacity) : newRecordLength + " > " + recordCapacity +
                                                         ": can't be, since recordContent.capacity()==recordCapacity!";
            recordLayout.putLength(buffer, offsetOnPage, newRecordLength);
            page.regionModified(offsetOnPage, recordLayout.headerSize() + newRecordLength);

            totalLiveRecordsPayloadBytes.addAndGet(newRecordLength - recordActualLength);
          }
          return currentRecordId;
        }
        finally {
          page.unlockPageForWrite();
        }
      }
    }
    throw new IOException("record[" + recordId + "].redirectTo chain is too long (>=" + MAX_REDIRECTS + "): circular reference?");
  }
@Override
  public int writeToRecord(final int recordId,
                           final @NotNull ByteBufferWriter writer,
                           final int expectedRecordSizeHint,
                           final boolean leaveRedirectOnRecordRelocation)