小服务器的悲哀,关于GraphicsMagick 137异常

用GraphicsMagick有一段时间了,一直以来都没有什么问题(最主要的原因是没有什么并发量),前一段时间出现过Empty input file的异常, 应该是下面这种情况导致的,两个线程请求缩略图,其中一个请求先完成操作,生成一张缩略图,但GraphicsMagick会先生成一张空白的缩略图文件,然后再向文件填充内容, 此时另一个线程判断缩略图存在,但还没有填充完毕,那么其实这是一个空白文件,这样就导致了EmptyInputFile异常。当时也没有多加考虑,采用的方法是先将缩略图写入一个临时文件, 写入完毕之后再将该临时文件移动到目标文件,但由于当时设计的临时文件相对缩略图是固定地址的,这样某些情况下移动文件时还会导致文件不存在的异常,最后一怒之下加了一把锁。

最近由于某些缩略图生成错误,所以干脆直接删除了整个缩略图文件夹,当我访问图片较多的文章,比如https://www.qyh.me/space/life/article/dog-life ,那个图片生成速度,简直了。。。 想了一下,把文件移动这个操作放到了GraphicsMagick操作类中:

IMOperation op = new IMOperation();
op.addImage();
setResize(resize, op);
String ext = FileUtils.getFileExtension(dest.getName());
if (!maybeTransparentBg(ext)) {
setWhiteBg(op);
}
op.strip();
op.p_profile("*");
if (interlace(dest)) {
op.interlace("Line");
}
op.addImage();
File temp = FileUtils.appTemp(ext);
run(op, src.getAbsolutePath(), temp.getAbsolutePath());
FileUtils.move(temp, dest);

这样看来并发的情况下应该不会有什么问题了(实际上,在测试过程中,生成同一个文件的缩略图时,在windows上依旧会出现各种各样的IO异常,但在linux上并没有发现)。 但是部署到服务器之后,由于同时生成缩略图的命令过多,又出现了

org.im4java.core.CommandException: org.im4java.core.CommandException: return code: 137 

最后,给生成缩略图的方法单独设置了一个线程池用来防止过多的缩略图操作,增加了ConcurrentHashMap用来防止同时生成相同的缩略图: ~~

package me.qyh.blog.file.local;

import static me.qyh.blog.file.ImageHelper.JPEG;
import static me.qyh.blog.file.ImageHelper.PNG;

import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

import me.qyh.blog.exception.SystemException;
import me.qyh.blog.file.ImageHelper;
import me.qyh.blog.file.Resize;
import me.qyh.blog.file.local.ImageResourceStore.ResizeStrategy;
import me.qyh.blog.util.FileUtils;

public class CachedResizeStrategy implements ResizeStrategy, ApplicationListener<ContextClosedEvent> {

	@Autowired
	private ImageHelper imageHelper;

	private final Map<String, Long> fileMap = new ConcurrentHashMap<>();
	private final Map<String, Long> coverMap = new ConcurrentHashMap<>();

	private static final Logger LOGGER = LoggerFactory.getLogger(CachedResizeStrategy.class);

	private final ExecutorService executor;

	public CachedResizeStrategy(int max) {
		if (max < 0) {
			throw new SystemException("最大执行线程数不能小于" + max);
		}
		executor = Executors.newFixedThreadPool(max);
	}

	public CachedResizeStrategy() {
		this(5);
	}

	@Override
	public void doResize(File local, File thumb, Resize resize) throws IOException {
		String ext = FileUtils.getFileExtension(local.getName());
		String coverExt = "." + (ImageHelper.maybeTransparentBg(ext) ? PNG : JPEG);
		File cover = new File(thumb.getParentFile(), FileUtils.getNameWithoutExtension(local.getName()) + coverExt);

		String coverCanonicalPath = cover.getCanonicalPath();
		String thumbCanonicalPath = thumb.getCanonicalPath();
        
        coverMap.compute(coverCanonicalPath, (ck, cv) -> {
				if (!cover.exists()) {
					executeFormat(local,cover)
				}
				return null;
			});

		fileMap.compute(thumbCanonicalPath, (k, v) -> {
			if (!thumb.exists()) {
				executeResize(cover,thumb,resize)
			}
			return null;
		});
		
	}

	private void executeFormat(File src, File dest) {
		CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
			try {
				FileUtils.forceMkdir(dest.getParentFile());
				imageHelper.format(src, dest);
			} catch (IOException e) {
				if (src.exists()) {
					LOGGER.error(e.getMessage(), e);
				}
			}
			return null;
		}, executor);
		future.join();
	}

	private void executeResize(File src, File dest, Resize resize) {
		CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
			try {
				FileUtils.forceMkdir(dest.getParentFile());
				imageHelper.resize(resize, src, dest);
			} catch (IOException e) {
				if (src.exists()) {
					LOGGER.error(e.getMessage(), e);
				}
			}
			return null;
		}, executor);
		future.join();
	}

	@Override
	public void onApplicationEvent(ContextClosedEvent event) {
		executor.shutdown();
		try {
			executor.awaitTermination(LIVE_MILL, TimeUnit.MICROSECONDS);
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
		}
	}

}

上面的compute方法是1.8新增的,如果是1.5+的环境,可以这样

private final Map<String, Boolean> fileMap = new ConcurrentHashMap<String,Boolean>();
if (fileMap.putIfAbsent(cPath, Boolean.TRUE) == null) {

	try {
		// 进行缩放
		executor.submit(resizeTask).get();
	} catch (ExecutionException e) {
    
    		throw new RuntimeException(e);
	} finally {
    
		// 无论缩放是否成功,删除key
		fileMap.remove(cPath);
	}
} else {

	// 如果有线程正在缩放目标图片,此时需要判断该图片是否已经生成
	// 如果没有生成完毕(与是否成功无关),等待一段时间
	while (fileMap.containsKey(cPath)) {
    
		try {
			Thread.sleep(MIN_SLEEP_MILL);
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
		}
	}
}

也许这样更好(参考:http://stackoverflow.com/questions/289434/how-to-make-a-java-thread-wait-for-another-threads-output):

private final Map<String, CountDownLatch> fileMap = new ConcurrentHashMap<String,CountDownLatch>();
if (fileMap.putIfAbsent(cPath, new CountDownLatch(1)) == null) {

	try {
		// 进行缩放
		executor.submit(resizeTask).get();
	} catch (ExecutionException e) {
    		throw new RuntimeException(e);
	} finally {
   		fileMap.get(cPath).countDown();
		// 无论缩放是否成功,删除key
		fileMap.remove(cPath);
	}
} else {

	// 如果有线程正在缩放目标图片,此时需要判断该图片是否已经生成
	// 如果没有生成完毕(与是否成功无关),等待一段时间
	CountDownLatch fromMap = fileMap.get(cPath);
    	if (fromMap != null) {
    		try {
    			fromMap.await();
    		} catch (InterruptedException e) {
    			Thread.currentThread().interrupt();
    		}
    	}
}

~~

2017.05.22 编辑
学艺不精啊,通过 Semaphore 即可方便的控制并发线程数,使用起来也非常简单:

Semaphore semaphore = new Semaphore(5);
if (!FileUtils.exists(thumb)) {
  try {
  	semaphore.acquire();
  } catch (InterruptedException e) {
  	Thread.currentThread().interrupt();
  	throw new SystemException(e.getMessage(), e);
  }
  try {
 	 FileUtils.forceMkdir(thumb.getParent());
 	 imageHelper.resize(resize, local, thumb);
  } finally {
  	semaphore.release();
  }
}

根本用不着上面这么繁琐。。。

参考代码: https://github.com/mhlx/mblog/blob/master/src/main/java/me/qyh/blog/support/file/local/ImageResourceStore.java