On previous meetup we discussed theoretical issue: How to develop application which allows to download few dozens small files from server, write some logs about it and execute http call if download was successful?
Actually task is quite easy but... we have few restrictions
1) We have only one Tomcat (means no cluster)
2) Expected number of downloads about 10 000 000 per day
So together we decided
1) Put files in cache 2) Execute async call to remote server Pretty simple. So I started implementing solution. First question was how to put file in cache. I didn't find any implemented solution and only one article.
Below my version of caching files
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
public class FileCacheAdapter extends CacheAdapter {
private static final Logger log = LoggerFactory.getLogger(FileCacheAdapter.class);
@Value(value = "#{'${file.path}'}")
private String filePath;
public InputStream getFile(String fileName) throws IOException {
StringBuilder lookupFile = new StringBuilder(filePath);
lookupFile.append("\\");
lookupFile.append(fileName);
Path filePath = Paths.get(lookupFile.toString());
boolean isExist = Files.exists(filePath);
if (!isExist) {
log.debug("File with fileName: {} was removed!", fileName);
remove(fileName);
return null;
}
long lastModified = Files.getLastModifiedTime(filePath).toMillis();
FileStamp fileStamp = get(fileName);
if (fileStamp != null && fileStamp.getLastModified() == lastModified) {
return new ByteArrayInputStream(fileStamp.getContent());
}
updateFile(fileName);
return getFile(fileName);
}
private void updateFile(String fileName) throws IOException{
remove(fileName);
putFile(fileName);
}
public void putFile(String fileName) throws IOException {
tryWriteLockOnKey(fileName, 2000L);
try {
if (get(fileName) != null) {
return;
}
StringBuilder lookupFile = new StringBuilder(filePath);
lookupFile.append(fileName);
Path filePath = Paths.get(lookupFile.toString());
byte[] fileContent = getFileContent(filePath);
long lastModified = Files.getLastModifiedTime(filePath).toMillis();
FileStamp fileStamp = new FileStamp(fileName, lastModified, fileContent);
put(fileName, fileStamp);
} finally {
releaseWriteLockOnKey(fileName);
}
}
private byte[] getFileContent(Path path) throws IOException {
byte[] fileContent = Files.readAllBytes(path);
return fileContent;
}
}
public class FileStamp implements Serializable {
/**
*
*/
private static final long serialVersionUID = -4056582522653888648L;
/**
* A name of the file
*/
private String fileName;
/**
* A time when the file was modified last time.
*/
private long lastModified;
/**
* A content of the file.
*/
private byte[] content;
public FileStamp() {
super();
}
public FileStamp(String fileName, long lastModified, byte[] content) {
this.fileName = fileName;
this.lastModified = lastModified;
this.content = content;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public long getLastModified() {
return lastModified;
}
public void setLastModified(long lastModified) {
this.lastModified = lastModified;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
}
Lets analyze step by step how it works. Fetching file from cache
public InputStream getFile(String fileName) throws IOException {
StringBuilder lookupFile = new StringBuilder(filePath);
lookupFile.append("\\");
lookupFile.append(fileName);
Path filePath = Paths.get(lookupFile.toString());
boolean isExist = Files.exists(filePath);
if (!isExist) {
log.debug("File with fileName: {} was removed!", fileName);
remove(fileName);
return null;
}
long lastModified = Files.getLastModifiedTime(filePath).toMillis();
FileStamp fileStamp = get(fileName);
if (fileStamp != null && fileStamp.getLastModified() == lastModified) {
return new ByteArrayInputStream(fileStamp.getContent());
}
updateFile(fileName);
return getFile(fileName);
}
I use new NIO API just to avoid file lock. As you see I check if file is present on filesystem and return null if there is no such file.
Then I check if file was modified and updated cache if it is needed.
Next method, actually three methods are responsible for inserting file in cache
Якщо потік не ортримає доступу за дві секунди то впаде LockTimeoutException, закачка зафейлиться Враховуючи що файли маленькі то це не повинно траплятись часто
* If can't get it in timeout millis then * return a boolean telling that it didn't get the lock треба дослідити уважніше це питання, бо у них якийсь трохи мес в документації... з одного боку обіцяють повернути булівський індекатор, а в документації на LockTimeoutException обіцють викинути ексепшин...
How to develop application which allows to download few dozens small files from server, write some logs about it and execute http call if download was successful?
яка відача? кому віддача? нащо? такого німає в постановці питання:)
nginx пише лог, його можна кастомізувати http://articles.slicehost.com/2010/8/27/customizing-nginx-web-logs можливо, вам слід буде робити якийсь постпроцесінг цих логів, щоб отримати потрібну статистику, але загалом цього має бути досить
не думаю що процесінг файлів з логами буде ефективніший ніж поточне рішення, хоч як варіант можливо, але знову ж таки читабельність, гнучкість впаде та й підтримка підозрюю буде складною
ключовий момент - те як конекшини на відадчу файлів будуть хендлитися томкетом та нджінксом. по суті, це як віддача звичайних статичних файл аля скриптів чи стилів - є серйозні причини використовувати проксі для цього
P.S. if (fileStamp != null && fileStamp.getLastModified() == lastModified) { return new ByteArrayInputStream(fileStamp.getContent()); } updateFile(fileName); return getFile(fileName); Looks a bit whacky - multiple returns, unnecessary recursion. Why not just have:
треба переврити результату tryWriteLockOnKey,
ВідповістиВидалитибо можна щось не то релізнути або потерти
але головне питання - навіщо ці файли класти в кеш? нехай собі на харді живуть
2) Expected number of downloads about 10 000 000 per day
Видалитиhttp://ehcache.org/documentation/apis/explicitlocking
ВидалитиtryWriteLockOnKey не довзоляє записувати в кеш одночасно для одного і того ж ключа
ага, але може так статися, що лок ми так і не отримаємо за 2 секунди - а далі код процесається без урахування цього моменту
ВидалитиЯкщо потік не ортримає доступу за дві секунди то впаде LockTimeoutException, закачка зафейлиться
ВидалитиВраховуючи що файли маленькі то це не повинно траплятись часто
* If can't get it in timeout millis then
Видалити* return a boolean telling that it didn't get the lock
треба дослідити уважніше це питання, бо у них якийсь трохи мес в документації... з одного боку обіцяють повернути булівський індекатор, а в документації на LockTimeoutException обіцють викинути ексепшин...
ага, ну і щодо ХТТП пеквесту на якийсь там урл - для цього nginx дозволяє конфігурити постакшин:
Видалитиpost_action /after_download;
location /after_download {
proxy_pass http://10.25.0.111/servlet.do?FileName=$request&ClientIP=$remote_addr......
}
@Konstantin Kudryavtsev
ВидалитиЗгідно сорсів ехкеша - впаде LockTimeoutException
мабуть nginx би і підійшов, але це не шлях Чака Норріса ;)
Швидкість віддачі інфи з кеша(пам"ять) вища ніж з харда.
ВідповістиВидалитиHow to develop application which allows to download few dozens small files from server, write some logs about it and execute http call if download was successful?
Видалитияка відача? кому віддача? нащо? такого німає в постановці питання:)
є 50 маленьких файлів на серваку пратягом дня їх скачують близько 10 млн раз
Видалитихм, а як же солюшини типу nginx, varnish? вони для цього і є
Видалитив нас задача трекати скачування і писати статистику я не в курсі як це можна зробити за допомогою nginx чи varnish, якщо є варіанти - кажи
Видалитиnginx пише лог, його можна кастомізувати http://articles.slicehost.com/2010/8/27/customizing-nginx-web-logs
Видалитиможливо, вам слід буде робити якийсь постпроцесінг цих логів, щоб отримати потрібну статистику, але загалом цього має бути досить
не думаю що процесінг файлів з логами буде ефективніший ніж поточне рішення, хоч як варіант можливо, але знову ж таки читабельність, гнучкість впаде та й підтримка підозрюю буде складною
Видалитиключовий момент - те як конекшини на відадчу файлів будуть хендлитися томкетом та нджінксом.
Видалитипо суті, це як віддача звичайних статичних файл аля скриптів чи стилів - є серйозні причини використовувати проксі для цього
короче, nginx wins by default :)
ну ти ж знаєш як воно, потім схочуть з UI вибирати шлях до стореджа, а потім ще щось і виявиться що на nginx це все дуже непросто
Видалитирозкажи потім, чи вдається віддавати 500-700 файлів на секунду
ВидалитиFileStamp could be made immutable (all fields final, remove setters, remove parameterless constructor).
ВідповістиВидалити"execute http call if download was successful" - so, how did you do that?
1) agree
Видалити2) will be in the second part
P.S.
ВідповістиВидалитиif (fileStamp != null && fileStamp.getLastModified() == lastModified) {
return new ByteArrayInputStream(fileStamp.getContent());
}
updateFile(fileName);
return getFile(fileName);
Looks a bit whacky - multiple returns, unnecessary recursion. Why not just have:
FileStamp fileStamp = get(fileName);
if (!(fileStamp != null && fileStamp.getLastModified() == lastModified)) { updateFile(filename); fileStamp = get(fileName); }
return fileStamp!=null?new ByteArrayInputStream(fileStamp.getContent()):null;
But it's a minor thing of course.
my version is more readable perhaps except return getFile(fileName);
Видалитиshould be return get(fileName);
of course should be return new ByteArrayInputStream(get(fileName).getContent());
Видалити