/**
* 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.hadoop.hdfs.qjournal.client;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.hdfs.server.protocol.NamespaceInfo;
import com.google.common.base.Joiner;
/**
* Output stream for uploading image to journal nodes. The stream maintains
* http-based channels to each of the nodes. It extends OutputStream, so it can
* be directly used for writing the image.
*/
public class HttpImageUploadStream extends OutputStream {
static final Log LOG = LogFactory.getLog(HttpImageUploadStream.class);
private final String journalId;
private final long txid;
private final List<HttpImageUploadChannel> uploadChannels
= new ArrayList<HttpImageUploadChannel>();
private ByteArrayOutputStream buffer;
private int flushSize;
public HttpImageUploadStream(List<String> uris, String journalId,
NamespaceInfo nsinfo, long txid, long epoch, int flushSize,
int maxBufferedChunks) throws IOException {
// create 20% larger buffer to minimize chances of expanding it
this.flushSize = flushSize;
this.buffer = new ByteArrayOutputStream((int) (1.2 * flushSize));
this.journalId = journalId;
this.txid = txid;
if (uris.size() % 2 == 0) {
throwIOException("The number of upload channels should not be even: "
+ uris.size());
}
for (String uri : uris) {
HttpImageUploadChannel ch = new HttpImageUploadChannel(uri, journalId,
nsinfo, txid, epoch, maxBufferedChunks);
uploadChannels.add(ch);
}
// initialize upload for each channel
for (HttpImageUploadChannel ch : uploadChannels) {
ch.start();
}
checkState();
}
public String toString() {
return "ImageUploadStream to [" + Joiner.on(",").join(uploadChannels)
+ "], for journal: " + journalId + ", txid= " + txid;
}
/**
* At each operation, this function is used to ensure that we still have a
* majority of successful channels to which we can write.
*/
void checkState() throws IOException {
int majority = getMajoritySize();
int numDisabled = 0;
for (HttpImageUploadChannel ch : uploadChannels) {
numDisabled += ch.isDisabled() ? 1 : 0;
}
if (numDisabled >= majority) {
Map<HttpImageUploadChannel, Void> successes
= new HashMap<HttpImageUploadChannel, Void>();
Map<HttpImageUploadChannel, Throwable> exceptions
= new HashMap<HttpImageUploadChannel, Throwable>();
for (HttpImageUploadChannel ch : uploadChannels) {
if (ch.isDisabled()) {
exceptions.put(ch, ch.getErrorStatus());
} else {
successes.put(ch, null);
}
}
throw QuorumException.create("Failed when uploading", successes,
exceptions);
}
}
/**
* Checks if the size of the temporary buffer exceeds maximum allowed size. If
* so, send the current buffer to the upload channels, and allocate new
* buffer.
*/
void checkBuffer() throws IOException {
if (buffer.size() >= flushSize) {
flushBuffer(false);
}
// check if we can continue
checkState();
}
/**
* Flushes the buffer to the upload channels and allocates a new buffer.
*/
private void flushBuffer(boolean close) {
for (HttpImageUploadChannel ch : uploadChannels) {
ch.send(buffer);
}
if (!close) {
// allocate new buffer, since the old one is used by the channels for
// reading
buffer = new ByteArrayOutputStream((int) (1.2 * flushSize));
}
}
/**
* @return the number of nodes which are required to obtain a quorum.
*/
int getMajoritySize() {
return uploadChannels.size() / 2 + 1;
}
static void throwIOException(String msg) throws IOException {
LOG.error(msg);
throw new IOException(msg);
}
// OutputStream methods
@Override
public void write(byte[] b) throws IOException {
buffer.write(b);
checkBuffer();
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
buffer.write(b, off, len);
checkBuffer();
}
@Override
public void write(int b) throws IOException {
buffer.write(b);
checkBuffer();
}
@Override
public void close() throws IOException {
// flush whatever we have left
flushBuffer(true);
for (HttpImageUploadChannel ch : uploadChannels) {
ch.close();
}
// check to see it the we have the quorum
checkState();
}
@Override
public void flush() throws IOException {
// send the buffer through the channels
flushBuffer(false);
// ensure that we still have a quorum
checkState();
}
}